nanpakal
nanpakal

Reputation: 1021

Spring Batch JUnit test for multiple jobs

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

Answers (11)

il-theCrow
il-theCrow

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:

  1. create a third Job bean marked as primary, hence JobLauncherTestUtils gets autowired with it;
  2. make the bean definition available to the current @SpringBatchTest class only.

Upvotes: 0

MrSpock
MrSpock

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

abhishek ringsia
abhishek ringsia

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

pete_bc
pete_bc

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

C. Asselmeyer
C. Asselmeyer

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

Xavier FRANCOIS
Xavier FRANCOIS

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

Puneet
Puneet

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

Tom&#225;š Mika
Tom&#225;š Mika

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

mad_fox
mad_fox

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

Ilya Dyoshin
Ilya Dyoshin

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

m.aibin
m.aibin

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

Related Questions