Reputation: 1021
I am having two jobs configured in one context file
<batch:job id="JobA" restartable="true">
<batch:step id="abc">
<batch:tasklet >
<batch:chunk reader="reader" writer="writer" processor="processor" />
</batch:tasklet>
</batch:step>
</batch:job>
<batch:job id="JobB" restartable="true">
<batch:step id="abc">
<batch:tasklet >
<batch:chunk reader="reader" writer="writer" processor="processor" />
</batch:tasklet>
</batch:step>
</batch:job>
When i am doing unit testing for the JobA using JobLauncherTestUtils
and testing the job launch it is throwing an exception saying
No unique bean of type [org.springframework.batch.core.Job;] is defined: expected single matching bean but found 2: [JobA, JobB]
i tried using @Qualifier
for autowire still the same thing. Where am i doing wrong here
edited
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:META-INF/spring/batch-test-context.xml" })
public class TestJob {
@Autowired
private JobExplorer jobExplorer;
@Autowired
@Qualifier("JobA")
private Job JobA;
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Test
public void testJob() throws Exception {
JobParameters jobParameters = getNextJobParameters(getJobParameters());
assertEquals(BatchStatus.COMPLETED, jobLauncherTestUtils.getJobLauncher().run(JobA, jobParameters));
}
private JobParameters getJobParameters() {
JobParametersBuilder jobParameters = new JobParametersBuilder();
jobParameters.addString("param", "123");
return jobParameters.toJobParameters();
}
private JobParameters getNextJobParameters(JobParameters jobParameters) {
String jobIdentifier = jobLauncherTestUtils.getJob().getName();
List<JobInstance> lastInstances = jobExplorer.getJobInstances(jobIdentifier, 0, 1);
JobParametersIncrementer incrementer = jobLauncherTestUtils.getJob().getJobParametersIncrementer();
if (lastInstances.isEmpty()) {
return incrementer.getNext(jobParameters);
} else {
List<JobExecution> lastExecutions = jobExplorer.getJobExecutions(lastInstances.get(0));
return incrementer.getNext(lastExecutions.get(0).getJobParameters());
}
}
}
exception was
No unique bean of type [org.springframework.batch.core.Job;] is defined: expected single matching bean but found 2: [JobA, JobB]`
Upvotes: 16
Views: 28869
Reputation: 1
I think this requires boot, but...
Assuming you have 2 Job
Spring beans, named jobA
and jobB
, you want to test jobA
.
@SpringBatchTest
@ContextConfiguration(MyMainClass.class) // load the application context as usual
@ContextConfiguration(JobATest.class) // load @Bean definition(s) below
class JobATest {
@Bean
@Primary
Job testJob(Job jobA) { // parameter name should be the component name
// otherwise you can use @Qualifier
return jobA;
}
// ... test methods
}
Explained, this configuration does:
Job
bean marked as primary, hence JobLauncherTestUtils
gets autowired with it;@SpringBatchTest
class only.Upvotes: 0
Reputation: 1717
I came up with a solution that doesn't involve JobLauncherTestUtils
at all. Looking at that class it didn't gave me much beside headaches.
First of all add an abstract base class:
@SpringBootTest
@Sql(
executionPhase = BEFORE_TEST_METHOD,
scripts = {"classpath:/org/springframework/batch/core/schema-h2.sql"})
@Sql(
executionPhase = AFTER_TEST_METHOD,
scripts = "classpath:/org/springframework/batch/core/schema-drop-h2.sql")
abstract class AbstractBatchIntegrationTest {
@Autowired private JobLauncher jobLauncher;
@Autowired private JobExplorer jobExplorer;
protected JobExecution startJob(final Job job) throws JobExecutionException {
return jobLauncher.run(job, new JobParametersBuilder(jobExplorer).getNextJobParameters(job).toJobParameters());
}
}
Hint: Add whatever Test-Annotations work for you. You can make use of @SpringBatchTest for example.
Note: As you can see the one-liner in startJob()
is all i need from JobLauncherTestUtils
The actual test looks like this:
class SampleJobIntegrationTest extends AbstractBatchIntegrationTest {
@Autowired private Job sampleJob;
@Test
void shouldStartSampleJob() throws Exception {
// given
// maybe prepare the database?
// when
final var jobExecution = startJob(sampleJob);
// then
assertThat(jobExecution.getExitStatus()).isEqualTo(ExitStatus.COMPLETED);
}
}
BTW: Many "guides" suggest to use @Qualifier
when autowiring your beans. Thats actually not necessary when you name your field correct.
Above example will work as long as you have a job bean somewhere in your production code:
@Bean
public Job sampleJob() {
return jobBuilder
.get("")
.incrementer(new RunIdIncrementer())
.flow(
...)
.end()
.build();
}
Pro: Clean and small test. Logic to start the job and all needed annotations are hidden in superclass.
Upvotes: 1
Reputation: 2050
I tried all these solution mentioned but nothing worked. After that i tried below
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles(value = { SPRING_PROFILE_TEST })
public class SIJobTest {
@Autowired
@Qualifier( "jobLauncherTestUtilsForTccLimit" )
protected JobLauncherTestUtils jobLauncherTestUtils;
in test case calling launch job
jobLauncherTestUtils.launchJob(params);
My Bean creation of jobLauncherTestUtils
@Configuration
public class SpringBatchTestConfiguration {
@Bean
public static JobLauncherTestUtils jobLauncherTestUtilsForTccLimit() {
return new TccLimitJobLauncherTestUtils();
}
public static class TccLimitJobLauncherTestUtils extends JobLauncherTestUtils {
@Override
@Autowired
public void setJob( @Qualifier("tcclimit_report") final Job job ) {
super.setJob( job );
}
}
}
Upvotes: 1
Reputation: 76
There is a discussion of this on the spring-batch github site.
The comment by manumouton sums up what worked for me. Basically, remove @SpringBatchTest
annotation from your test class if you are using it, and, for each Job you want to test, add a Bean in your test configuration to provide a separate JobLauncherTestUtils implementation that overrides the @Autowired
setJob(Job job) method to add @Qualifier
that identifies the particular job you want to target.
This is similar to ilya-dyoshin's solution, but better satisfies one's obsession with IOC.
I'm just going to copy and paste the code from github for convenience:
@Configuration
static class MyTestConfiguration {
@Bean
public JobLauncherTestUtils myJobLauncherTestUtils() {
return new JobLauncherTestUtils() {
@Override
@Autowired
public void setJob(@Qualifier("mySpecificJobQualifier") Job job) {
super.setJob(job);
}
};
}
}
Upvotes: 2
Reputation: 81
When using Spring Boot I would recommend to separate the two jobs into separate contexts using @EnableBatchProcessing(modular = true)
Jobs can then be tested separately by providing only the specific configuration with @SpringBootTest(classes = { JobAConfiguration.class, ... })
There is a great and comprehensive example including separate test classes for separate jobs at https://github.com/desprez/springbatch-modular (not by me, kudos to the author).
Upvotes: 1
Reputation: 712
We faced the same problem because we also use spring-cloud-configuration
in the project, which needs @SpringBootTest
annotation, and then loads the entire Spring Boot context, so multiple jobs are loaded in the context.
The solution we came to is close to the one provided by Ilya Dyoshin, with constructor injection and Junit 5 :
@ExtendWith(SpringExtension.class)
@SpringBootTest
class MyJobConfigTest {
private final JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
public MyJobConfigTest(Job jobNumber1, JobLauncher jobLauncher, JobRepository jobRepository) {
this.jobLauncherTestUtils = new JobLauncherTestUtils();
this.jobLauncherTestUtils.setJobLauncher(jobLauncher);
this.jobLauncherTestUtils.setJobRepository(jobRepository);
this.jobLauncherTestUtils.setJob(jobNumber1);
}
Upvotes: 0
Reputation: 194
Not answer to original problem, but using below code we have avoided reuse of JobLauncherTestUtils during sequential run of test cases in same class.
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
This indicates Junit to clean up and re construct context after each run.
Upvotes: 2
Reputation: 330
I solved it by creating JobLauncherTestUtils for each job separately (groovy):
@TestConfiguration class BatchJobTestConfiguration {
@Autowired
@Qualifier('job1')
private Job job1
@Autowired
@Qualifier('job2')
private Job job2
@Autowired
JobRepository jobRepository;
@Bean
JobLauncher jobLauncher() throws Exception {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.setTaskExecutor(new SyncTaskExecutor());
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
@Bean(name = 'jobLauncherTestUtilsJob1')
JobLauncherTestUtils jobLauncherTestUtilsSyncEndUserJob() {
new JobLauncherNoAutowireTestUtil(
job: job1,
jobLauncher: jobLauncher()
)
}
@Bean(name = 'jobLauncherTestUtilsJob2')
JobLauncherTestUtils jobLauncherTestUtilsenewCaseJob() {
new JobLauncherNoAutowireTestUtil(
job: job2,
jobLauncher: jobLauncher()
)
}
Then add this into your test:
@ContextConfiguration(classes = [BatchJobTestConfiguration])
...
@Autowired
@Qualifier('jobLauncherTestUtilsJob1')
private JobLauncherTestUtils jobLauncherTestUtils
...
when:
def jobExecution = jobLauncherTestUtils.launchJob()
Upvotes: 1
Reputation: 3188
Because there is an @Autowired
annotation on the setter for JobLauncherTestUtils.setJob(Job job)
I had to use a MergedBeanDefinitionPostProcessor to set the property after the bean was created:
@Configuration
public class TestBatchConfiguration implements MergedBeanDefinitionPostProcessor {
@Autowired
@Qualifier("JobA")
private Job job;
@Bean(name="jtestl")
public JobLauncherTestUtils jobLauncherTestUtils() {
JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
jobLauncherTestUtils.setJob(job);
return jobLauncherTestUtils;
}
/**
* https://stackoverflow.com/questions/22416140/autowire-setter-override-with-java-config
* This is needed to inject the correct job into JobLauncherTestUtils
*/
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
if(beanName.equals("jtestl")) {
beanDefinition.getPropertyValues().add("job", getMyBeanFirstAImpl());
}
}
private Object getMyBeanFirstAImpl() {
return job;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
Upvotes: 5
Reputation: 4624
Maybe late,
but I found for myself working solution: manual configuration of JobLauncherTestUtils
:
@Inject
@Qualifier(value = "Job1")
private Job job;
@Inject
private JobLauncher jobLauncher;
@Inject
private JobRepository jobRepository;
private JobLauncherTestUtils jobLauncherTestUtils;
private void initailizeJobLauncherTestUtils() {
this.jobLauncherTestUtils = new JobLauncherTestUtils();
this.jobLauncherTestUtils.setJobLauncher(jobLauncher);
this.jobLauncherTestUtils.setJobRepository(jobRepository);
this.jobLauncherTestUtils.setJob(job);
}
@Before
public void setUp() throws Exception {
this.initailizeJobLauncherTestUtils();
}
with this you can control for which Job should JobLauncherTestUtils be applied. (by default it expects single Job configuration in context)
Upvotes: 16
Reputation: 3593
You have two similar beans declared in bean configuration file.
To fix above problem, you need @Qualifier("JobA")
and @Qualifier("JobB")
to tell Spring about which bean should auto wired to which job.
Upvotes: 1