Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Job unit testing with @SpringBatchTest #3699

Closed
tiparega opened this issue Apr 20, 2020 · 9 comments
Closed

Multiple Job unit testing with @SpringBatchTest #3699

tiparega opened this issue Apr 20, 2020 · 9 comments

Comments

@tiparega
Copy link

Hi all,

From documentation, it seems impossible to unit test a Spring Batch with more than one job, problem is JobLauncherTestUtils autowiring the Job. This can be resolved as in #1237 but, as docs state to use @SpringBatchTest (#889), a default JobLauncherTestUtils bean is created in BatchTestContextCustomizer so, even if you don't use any JobLauncherTestUtils at all, it tries to create one and fails on no single Job bean.

Not sure if this has a simple resolution, may be deleting autowired Job in JobLauncherTestUtils, or allowing some kind of @SpringBatchTest customization, but I think that it could be, at least, mentioned in unit testing docs, so to not use @SpringBatchTest annotation if you have more than one Job bean.

@mminella
Copy link
Member

The utility in question JobLauncherTestUtils is for testing a job. That implies that the context it is wired into only has one (or is marked via @Primary for Spring's autowiring functionality). I'd be interested in hearing the use case you are exploring where a test of a job ends up with more than one job that cannot be marked as @Primary.

@jblayneyXpanxion
Copy link

jblayneyXpanxion commented May 22, 2020

@mminella

So I might chime in here. I'm pretty good at spring and setting up integration tests, and setting up spring batch tests has been quite the nightmare.

I have a spring boot web application that is also enabled for spring batch.

A few issues I've run into:

  • If you add @SpringBatchTest to any test that uses a context configuration that uses your main app's main class.. eg: Application.java that may have multiple jobs in the component scan (common), then this explodes. There is no perfect way to exclude certain sets of jobs from the main app's context for certain tests so there is only a single job bean. Overriding the bean or defining the jobLauncherTestUtils explicitly doesn't work either, as apparently it still tries to autowire just one job and explodes before the overriding can kick in.
  • If you remove any inheritance/include of a spring boot context configuration, then the jobLauncherTestUtils won't autowire. You can find a lot of discussion on this by searching stackoverflow for "jobLauncherTestUtils is null". A perfect example would be to verbatim use the setup that the SpringBatchTest.java shows in its javadoc where the (MyBatchJobConfiguration.class)/ ContextConfiguration(classes = MyBatchJobConfiguration.class) is a @Configuration class with added @EnableBatchProcessingwith only Bean definitions for jobs and steps. It doesn't work - the autowiring is null unless you happen to configure the MyBatchJobConfiguration to also be a custom SpringBootApplication for that particular test. As Stackoverflow posts seem to indicate that you'll need @SpringBootApplication somewhere in the config to make the autowiring work as its supposed to for SpringBatchTest. So I guess instead of using the context for the whole app (with multiple jobs), we'll have to define a separate spring boot application for each test that is careful to only import one job.
  • If you instead use @SpringBootTest ... or alternatively both of @RunWith(SpringRunner.class) with @ContextConfiguration(classes = { Application.class, TestJobConfiguration.class }), then I'll often get this great error: Scope 'step' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton even though the @EnableBatchProcessing annotation is properly included in the application configuration.

So I guess from where I'm sitting, I'm wondering why force the developer to wire only one bean by creating custom spring boot applications for each test when Spring Integration Tests are designed to re-use the context?

Also, what would the Configuration Class for MyBatchJobConfiguration look like to make the code in the SpringBatchTest.java's javadoc actually work and autowire the test classes? It must be more than a Job Configuration class - it would have to configure/setup a spring boot application, or have a context configuration which does (and also doesn't wire in a second job via any downstream component scans or includes)..

So I think the intention here was to make testing easy, and its likely simple to use for simple applications or proof of concepts, but I find myself tearing my hair out trying to get anything to work right in a real world application.

I'll try out your @Primary suggestion which I hope will work (I'll try to let you know if it does), however this would eliminate the ability to mark any Job as primary in the main application if the tests were to use the same context. So if there is a need to have spring batch tests, you should never mark a job bean as Primary unless its is in an integration test's configuration file.

Thanks! There is a lot there but hopefully it might be helpful feedback to have about trying to get anything working around these use cases.

@manumouton
Copy link

@jblayneyXpanxion
Actually, the only way I've found to deal with complex application context (web, data, batch, ...) and multiple jobs defined is to avoid the @SpringBatchTest annotation and to redefine for all test classes an inner static @Configuration class which defines a JobLauncherTestUtils bean for the concerned job.

@Configuration
  static class MyTestConfiguration {
    @Bean
    public JobLauncherTestUtils myJobLauncherTestUtils() {
      return new JobLauncherTestUtils() {
        @Override
        @Autowired
        public void setJob(@Qualifier("mySpecificJobQualifier") Job job) {
          super.setJob(job);
        }
      };
    }
  }

@jblayneyXpanxion
Copy link

@manumouton thanks! It helps to get any feedback on this.

I've tried this intermittently trying to get things going, and I have steps created with the @StepScope so that they can use JobParameters on construction, and this setup works and even autowires the jobLauncher and jobRepository into the jobLauncherTestUtils which is great.

So thanks!

Another thing that doesn't seem to be documented very well in any spring tutorials is the use of:

JobExecution jobExecution = jobLauncherTestUtils.launchJob(params);

This seems to asynchronously launch the job -> It won't run it and return the finished execution as the examples seem to imply.

Running the test straight through references the jobExecution which hasn't run yet or hasn't completed, and I'll often get a stack trace in the output which has this error: Error creating bean with name 'jobBeanName': Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)

However when I debug the test, I can see the full job execution output on the console and I don't see this error, so I believe that debugging line by line gives the job time to complete before all of the assertions are run.

The only thing I can think to do for this is to run a loop waiting for completion of the job with some sleep statements, which isn't ideal.

It seems odd that spring's examples and tutorials would show immediate assertions on the jobExecution right after launch if this causes a race condition.

Would also be nice to see a helper method in jobLauncherTestUtils to wait for a job's execution to complete.

Thanks again, and any additional feedback would be welcome.

@manumouton
Copy link

manumouton commented May 27, 2020

@manumouton thanks! It helps to get any feedback on this.

👍 you're welcome.

Another thing that doesn't seem to be documented very well in any spring tutorials is the use of:

JobExecution jobExecution = jobLauncherTestUtils.launchJob(params);

This seems to asynchronously launch the job -> It won't run it and return the finished execution as the examples seem to imply.
...
The only thing I can think to do for this is to run a loop waiting for completion of the job with some sleep statements, which isn't ideal.

Unfortunately, this is also the only way I can see right now.

 while (jobExecution.getStatus() == BatchStatus.STARTING || jobExecution.isRunning() || jobExecution.isStopping()) {
//          log.debug("Batch is still running");
    }
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());

Would also be nice to see a helper method in jobLauncherTestUtils to wait for a job's execution to complete.

I totally agree!

Thanks @jblayneyXpanxion
Sorry @mminella & @benas, this thread should possibly continue elsewhere since not related to the initial request.

@Insomnium
Copy link

Guys, there's always a way to leave single Job instance in exact test's ApplicationContext, like using profiles. But that could turn configuration and deployment into mess when there's really a lot of jobs in application.
Could you get rid of Job autowiring in JobLauncherTestUtils somehow?
Maybe you could extend test class-level annotation like SpringBatchTest with an argument dedicated to exact Job bean name, or add a brand new one instead?

@fmbenhassine
Copy link
Contributor

The same behaviour happens if multiple datasources are defined (which is a common case) since @SpringBatchTest also registers a JobRepositoryTestUtils bean in the test context, in which a datasource is autowired.

JobLauncherTestUtils and JobRepositoryTestUtils autowire other beans like JobLauncher and JobRepository, and even if it's unlikely to have multiple JobLauncher and JobRepository beans, it's technically possible to have the same behaviour if multiple beans of these types are defined in the test context. So the issue is not directly related to @SpringBatchTest, but rather to how the utility classes it imports are designed to work.

I think that it could be, at least, mentioned in unit testing docs, so to not use @SpringBatchTest annotation if you have more than one Job bean.

Based on what Michael said, I will update the documentation to explicitly mention that the test context is expected to have a single autowire candidate for the job (and datasource).

Removing Job autowiring in JobLauncherTestUtils (requested in #1237) and Datasource autowiring in JobRepositoryTestUtils means that manual wiring of these dependencies will be required from the user, which I think was the main motivation behind using autowiring in the first place.

That said, I believe the root cause of the issue is importing an application context with multiple jobs in the same test class (ie trying to test multiple jobs in the same test class). Following the unix philosophy of making one thing do one thing and do it well, I would define each job in its own configuration class, something like:

@EnableBatchProcessing
class BaseJobConfiguration {
   // common job infrastructure beans (datasource, job repository, etc)
}

@Configuration
class Job1Configuration extends BaseJobConfiguration {
   // beans needed by job1
   @Bean
   public Job job1() {};
}

@Configuration
class Job2Configuration extends BaseJobConfiguration {
   // beans needed by job2
   @Bean
   public Job job2() {};
}

and test each job in its own test class as well:

@RunWith(SpringRunner.class)
@SpringBatchTest
@ContextConfiguration(classes = {Job1Configuration.class})
public class Job1Tests {

	@Autowired
	private JobLauncherTestUtils jobLauncherTestUtils;

	@Test
	public void testSunnyDay()  {}
	@Test
	public void testFailureCase()  {}
	// other tests for job1
}

// same for Job2Tests

I think it's a good practice to isolate job definitions and their corresponding tests, which solves the issue by design. The fact that @SpringBatchTest makes it difficult to not follow these best practices is a good point IMO.

Also, what would the Configuration Class for MyBatchJobConfiguration look like to make the code in the SpringBatchTest.java's javadoc actually work and autowire the test classes?

The example in the Javadoc of @SpringBatchTest follows the aforementioned approach (notice the usage of singular names in MyBatchJobConfiguration and MyBatchJobTests).

Another thing that doesn't seem to be documented very well in any spring tutorials is the use of:
JobExecution jobExecution = jobLauncherTestUtils.launchJob(params);
This seems to asynchronously launch the job

JobLauncherTestUtils uses a JobLauncher (which is autowired) to launch jobs. By default when using @EnableBatchProcessing, a synchronous JobLauncher is used. So if you see jobLauncherTestUtils.launchJob() running asynchronously, you should have an asynchronous JobLauncher in your application context that is autowired in JobLauncherTestUtils. For this kind of questions, please use StackOverflow (I can help if you provide a minimal complete example).

@desprez
Copy link

desprez commented Mar 2, 2021

May be late but i made this example to demonstrate "Multiple Job unit testing with @SpringBatchTest @SpringBootTest and Modular configuration"

https://github.com/desprez/springbatch-modular

I think this case may be more common than you think and hope it can help others

@chomookun
Copy link

Hope this helps someone ^^;

Abstract class for test

@Slf4j
@SpringBootTest(
        classes = BatchApplication.class
)
@SpringBatchTest
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
public class BatchTestSupport {

    @Autowired
    private JobRepository jobRepository;

    @Autowired
    private JobLauncher jobLauncher;

    protected final JobLauncherTestUtils getJobLauncherTestUtils(Job job) {
        JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
        jobLauncherTestUtils.setJobRepository(jobRepository);
        jobLauncherTestUtils.setJobLauncher(jobLauncher);
        jobLauncherTestUtils.setJob(job);
        return jobLauncherTestUtils;
    }

JUnit Test

@Slf4j
@ContextConfiguration(classes= DatabaseToDatabase.class)
public class DatabaseToDatabaseTest extends BatchTestSupport {

    @Autowired
    @Qualifier("querydslToJpaJob")
    Job querydslToJpaJob;

    @Autowired
    @Qualifier("mybatisToMybatisJob")
    Job mybatisToMybatisJob;

    @Test
    public void testQuerydslToJpaJob() throws Exception {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("size", 123L)
                .toJobParameters();
        JobExecution jobExecution = getJobLauncherTestUtils(querydslToJpaJob).launchJob(jobParameters);
        assertEquals(BatchStatus.COMPLETED.name(), jobExecution.getStatus().name());
    }

    @Test
    public void testMybatisToMybatisJob() throws Exception {
        JobParameters jobParameters = new JobParametersBuilder()
                .addLong("size", 123L)
                .toJobParameters();
        JobExecution jobExecution = getJobLauncherTestUtils(mybatisToMybatisJob).launchJob(jobParameters);
        assertEquals(BatchStatus.COMPLETED.name(), jobExecution.getStatus().name());
    }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants