I am working on a Spring Batch application. Untill now I was able to unit test something like service methods and something like this (as done in every Spring Boot application).
Now I am trying to follow this tutorial in order to test an entire job from my unit test class (basically I want to execute a test method that perform a Job): https://www.baeldung.com/spring-batch-testing-job
This is my JUnit test class, in this case works fine and I correctly can test my services method using the @SpringBootTest annotation:
@SpringBootTest @SpringBatchTest class UpdateInfoBatchApplicationTests { @Autowired private NotaryService notaryService; @Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Autowired private JobRepositoryTestUtils jobRepositoryTestUtils; @After public void cleanUp() { jobRepositoryTestUtils.removeJobExecutions(); } @Autowired @Qualifier("launcher") private JobLauncher jobLauncher; @Autowired @Qualifier("updateNotaryDistrictsJob") private Job updateNotaryDistrictsJob; @Autowired @Qualifier("updateNotaryListInfoJob") private Job updateNotaryListInfoJob; private JobParameters defaultJobParameters() { JobParametersBuilder paramsBuilder = new JobParametersBuilder(); //paramsBuilder.addString("file.input", TEST_INPUT); //paramsBuilder.addString("file.output", TEST_OUTPUT); return paramsBuilder.toJobParameters(); } @Test public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception { // when JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters()); JobInstance actualJobInstance = jobExecution.getJobInstance(); ExitStatus actualJobExitStatus = jobExecution.getExitStatus(); Assert.assertEquals(actualJobInstance.getJobName(), "updateNotaryDistrictsJob"); // then //assertThat(actualJobInstance.getJobName(), is("updateNotaryDistrictsJob")); //assertThat(actualJobExitStatus.getExitCode(), is("COMPLETED")); //AssertFile.assertFileEquals(expectedResult, actualResult); } @Test void contextLoads() { System.out.println("TEST - contextLoads()"); } @Test void getNotaryList() throws Exception { List<Notary> notaryList = this.notaryService.getNotaryList(); System.out.println("notaryList size: " + notaryList); Assert.assertEquals("Notary List must be 5069", 5069, notaryList.size()); } @Test void getNotaryDetails() throws Exception { NotaryDetails notaryDetails = this.notaryService.getNotaryDetails("089cy5Ra9zE%253D"); System.out.println("notaryDetails: " + notaryDetails); Assert.assertEquals("Notary ID must be 089cy5Ra9zE%253D", "089cy5Ra9zE%253D", notaryDetails.getIdNotary()); } @Test void getNotaryDistrictsList() throws Exception { List<NotaryDistrict> notaryDistrictsList = this.notaryService.getNotaryDistrictsList(); System.out.println("notaryDistrictsList: " + notaryDistrictsList); Assert.assertEquals("Notary districts list lenght must be 91", 91, notaryDistrictsList.size()); //ArrayList<NotaryDistrict> notaryDistrictsListArrayList = new ArrayList<NotaryDistrict>(notaryDistrictsList); notaryDistrictsList.remove(0); Assert.assertEquals("Notary districts list lenght must now be 90", 90, notaryDistrictsList.size()); } @Test void getNotaryDistrictDetails() throws Exception { NotaryDistrictDetails notaryDistrictDetails = this.notaryService.getNotaryDistrictDetails("CG7drXn9fvA%253D"); System.out.println("notaryDistrictDetails: " + notaryDistrictDetails.toString()); Assert.assertEquals("Distretto must be: SCIACCA", "SCIACCA", notaryDistrictDetails.getDistretto()); } }
As you can see in the previous code I first inject my two Job objects defined:
@Autowired @Qualifier("launcher") private JobLauncher jobLauncher; @Autowired @Qualifier("updateNotaryDistrictsJob") private Job updateNotaryDistrictsJob;
These Job are defined as bean into the class that configures my Spring Batch jobs and steps, basically I have these 2 beans:
@Bean("updateNotaryDistrictsJob") public Job updateNotaryDistrictsListInfoJob(){ return jobs.get("updateNotaryDistrictsListInfoJob") .incrementer(new RunIdIncrementer()) .start(readNotaryDistrictsListStep()) .build(); }
and
@Bean("updateNotaryListInfoJob") public Job updateNotaryListInfoJob(){ return jobs.get("updateNotaryListInfoJob") .incrementer(new RunIdIncrementer()) .start(readNotaryListStep()) .build(); }
Then in the previous test class there is this test method that should test the entire flow of the previous updateNotaryDistrictsJob job:
@Test public void givenReferenceOutput_whenJobExecuted_thenSuccess() throws Exception { // when JobExecution jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters()); JobInstance actualJobInstance = jobExecution.getJobInstance(); ExitStatus actualJobExitStatus = jobExecution.getExitStatus(); Assert.assertEquals(actualJobInstance.getJobName(), "updateNotaryDistrictsJob"); }
The problem is that doing in this way when I run this test method I am obtaining this exception in my stack trace:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jobLauncherTestUtils': Unsatisfied dependency expressed through method 'setJob' parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.batch.core.Job' available: expected single matching bean but found 2: updateNotaryDistrictsJob,updateNotaryListInfoJob at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:768) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:720) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1413) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:601) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:944) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.9.jar:5.3.9] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.9.jar:5.3.9] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-2.5.3.jar:2.5.3] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:434) ~[spring-boot-2.5.3.jar:2.5.3] at org.springframework.boot.SpringApplication.run(SpringApplication.java:338) ~[spring-boot-2.5.3.jar:2.5.3] at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:123) ~[spring-boot-test-2.5.3.jar:2.5.3] at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) ~[spring-test-5.3.9.jar:5.3.9] at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124) ~[spring-test-5.3.9.jar:5.3.9] ... 69 common frames omitted Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.batch.core.Job' available: expected single matching bean but found 2: updateNotaryDistrictsJob,updateNotaryListInfoJob at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1358) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300) ~[spring-beans-5.3.9.jar:5.3.9] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.resolveMethodArguments(AutowiredAnnotationBeanPostProcessor.java:760) ~[spring-beans-5.3.9.jar:5.3.9] ... 88 common frames omitted
It seems that it can’t recognize what of my two Job beans must be used.
Why? What is wrong? What am I missing? How can I try to fix this issue?
Advertisement
Answer
The JobLauncherTestUtils
that is provided by @SpringBatchTest
expects that there is only a single bean of type Job
in the test context. This is also documented in the java doc of the annotation.
If you use @SpringBootTest
and full component scanning such that more than one job bean is picked up, @SpringBatchTest
does not work out of the box.
The easiest solution is probably to remove @SpringBatchTest
and to start the jobs with the jobLauncher
. Alternatively, you can split your tests across multiple test classes and use test contexts that only contain a single job bean, respectively.