Add a new annotation @SpringBatchTest to simplify testing

This commit adds a new annotation `@SpringBatchTest` that:

* configures two beans `JobLauncherTestUtils` and `JobRepositoryTestUtils`
in the test context.

* imports the `StepScopeTestExecutionListener` and
`JobScopeTestExecutionListener` as test execution listeners.

Resolves BATCH-2718
This commit is contained in:
Mahmoud Ben Hassine
2018-05-17 13:31:41 +02:00
committed by Michael Minella
parent 98070e4a1c
commit 0dab67c936
10 changed files with 536 additions and 9 deletions

View File

@@ -497,6 +497,7 @@ project('spring-batch-test') {
testCompile "org.apache.commons:commons-dbcp2:$commonsDdbcpVersion"
testCompile "org.hsqldb:hsqldb:$hsqldbVersion"
testCompile "org.mockito:mockito-core:$mockitoVersion"
optional "org.aspectj:aspectjrt:$aspectjVersion"
optional "javax.batch:javax.batch-api:$javaxBatchApiVersion"

View File

@@ -28,19 +28,24 @@ In order for the unit test to run a batch job, the framework must
this behavior:
* `@RunWith(SpringJUnit4ClassRunner.class)`:
* `@RunWith(SpringRunner.class)`:
Indicates that the class should use Spring's JUnit facilities
* `@ContextConfiguration(...)`:
Indicates which resources to configure the `ApplicationContext` with.
The following example shows the two annotations in use:
Starting from v4.1, it is also possible to inject Spring Batch test utilities
like the `JobLauncherTestUtils` and `JobRepositoryTestUtils` in the test context
using the `@SpringBatchTest` annotation.
The following example shows the annotations in use:
.Using Java Configuration
[source, java, role="javaContent"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests { ... }
----
@@ -48,7 +53,8 @@ public class SkipSampleFunctionalTests { ... }
.Using XML Configuration
[source, java, role="xmlContent"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests { ... }
@@ -81,7 +87,8 @@ In the following example, the batch job reads from the database and
.XML Based Configuration
[source, java, role="xmlContent"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })
public class SkipSampleFunctionalTests {
@@ -115,7 +122,8 @@ public class SkipSampleFunctionalTests {
.Java Based Configuration
[source, java, role="javaContent"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes=SkipSampleConfiguration.class)
public class SkipSampleFunctionalTests {
@@ -187,7 +195,7 @@ The listener is declared at the class level, and its job is to
@ContextConfiguration
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
StepScopeTestExecutionListener.class })
@RunWith(SpringJUnit4ClassRunner.class)
@RunWith(SpringRunner.class)
public class StepScopeTestExecutionListenerIntegrationTests {
// This component is defined step-scoped, so it cannot be injected unless
@@ -195,7 +203,7 @@ public class StepScopeTestExecutionListenerIntegrationTests {
@Autowired
private ItemReader<String> reader;
public StepExecution getStepExection() {
public StepExecution getStepExecution() {
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
return execution;
@@ -222,6 +230,38 @@ There are two `TestExecutionListeners`. One
`StepExecution`). If a factory method is not provided,
then a default `StepExecution` is created.
Starting from v4.1, the `StepScopeTestExecutionListener` and
`JobScopeTestExecutionListener` are imported as test execution listeners
if the test class is annotated with `@SpringBatchTest`. The preceding test
example can be configured as follows:
[source, java]
----
@SpringBatchTest
@RunWith(SpringRunner.class)
@ContextConfiguration
public class StepScopeTestExecutionListenerIntegrationTests {
// This component is defined step-scoped, so it cannot be injected unless
// a step is active...
@Autowired
private ItemReader<String> reader;
public StepExecution getStepExecution() {
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
return execution;
}
@Test
public void testReader() {
// The reader is initialized and bound to the input data
assertNotNull(reader.read());
}
}
----
The listener approach is convenient if you want the duration of the
step scope to be the execution of the test method. For a more flexible
but more invasive approach, you can use the

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2007 the original author or authors.
* Copyright 2006-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.dao.AbstractJdbcBatchMetadataDao;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -49,6 +50,7 @@ import org.springframework.util.Assert;
* the transaction.
*
* @author Dave Syer
* @author Mahmoud Ben Hassine
*/
public class JobRepositoryTestUtils extends AbstractJdbcBatchMetadataDao implements InitializingBean {
@@ -95,6 +97,7 @@ public class JobRepositoryTestUtils extends AbstractJdbcBatchMetadataDao impleme
setDataSource(dataSource);
}
@Autowired
public final void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@@ -109,6 +112,7 @@ public class JobRepositoryTestUtils extends AbstractJdbcBatchMetadataDao impleme
/**
* @param jobRepository the jobRepository to set
*/
@Autowired
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test.context;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.JobRepositoryTestUtils;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.util.Assert;
/**
* {@link ContextCustomizer} implementation that adds batch test utility classes
* ({@link JobLauncherTestUtils} and {@link JobRepositoryTestUtils}) as beans in
* the test context.
*
* @author Mahmoud Ben Hassine
* @since 4.1
*/
public class BatchTestContextCustomizer implements ContextCustomizer {
private static final String JOB_LAUNCHER_TEST_UTILS_BEAN_NAME = "jobLauncherTestUtils";
private static final String JOB_REPOSITORY_TEST_UTILS_BEAN_NAME = "jobRepositoryTestUtils";
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,
"The bean factory must be an instance of BeanDefinitionRegistry");
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
registry.registerBeanDefinition(JOB_LAUNCHER_TEST_UTILS_BEAN_NAME,
new RootBeanDefinition(JobLauncherTestUtils.class));
registry.registerBeanDefinition(JOB_REPOSITORY_TEST_UTILS_BEAN_NAME,
new RootBeanDefinition(JobRepositoryTestUtils.class));
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test.context;
import java.util.List;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
/**
* Factory for {@link BatchTestContextCustomizer}.
*
* @author Mahmoud Ben Hassine
* @since 4.1
*/
public class BatchTestContextCustomizerFactory implements ContextCustomizerFactory {
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
SpringBatchTest springBatchTest = AnnotatedElementUtils.findMergedAnnotation(testClass, SpringBatchTest.class);
if (springBatchTest != null) {
return new BatchTestContextCustomizer();
}
return null;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test.context;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.batch.test.JobRepositoryTestUtils;
import org.springframework.batch.test.JobScopeTestExecutionListener;
import org.springframework.batch.test.StepScopeTestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
/**
* Annotation that can be specified on a test class that runs Spring Batch based tests.
* Provides the following features over the regular <em>Spring TestContext Framework</em>:
* <ul>
* <li>Registers a {@link JobLauncherTestUtils} bean with the
* {@link BatchTestContextCustomizer#JOB_LAUNCHER_TEST_UTILS_BEAN_NAME} which can be used
* in tests for launching jobs and steps.
* </li>
* <li>Registers a {@link JobRepositoryTestUtils} bean
* with the {@link BatchTestContextCustomizer#JOB_REPOSITORY_TEST_UTILS_BEAN_NAME}
* which can be used in tests setup to create or remove job executions.
* </li>
* <li>Registers the {@link StepScopeTestExecutionListener} and {@link JobScopeTestExecutionListener}
* as test execution listeners which are required to test step/job scoped beans.
* </li>
* </ul>
* <p>
* A typical usage of this annotation is like:
*
* <pre class="code">
* &#064;RunWith(SpringRunner.class)
* &#064;SpringBatchTest
* &#064;ContextConfiguration(classes = MyBatchJobConfiguration.class)
* public class MyBatchJobTests {
*
* &#064;@Autowired
* private JobLauncherTestUtils jobLauncherTestUtils;
*
* &#064;@Autowired
* private JobRepositoryTestUtils jobRepositoryTestUtils;
*
* &#064;Before
* public void clearJobExecutions() {
* this.jobRepositoryTestUtils.removeJobExecutions();
* }
*
* &#064;Test
* public void testMyJob() throws Exception {
* // given
* JobParameters jobParameters = this.jobLauncherTestUtils.getUniqueJobParameters();
*
* // when
* JobExecution jobExecution = this.jobLauncherTestUtils.launchJob(jobParameters);
*
* // then
* Assert.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
* }
*
* }
* </pre>
*
* @author Mahmoud Ben Hassine
* @since 4.1
* @see JobLauncherTestUtils
* @see JobRepositoryTestUtils
* @see StepScopeTestExecutionListener
* @see JobScopeTestExecutionListener
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@TestExecutionListeners(
listeners = {StepScopeTestExecutionListener.class, JobScopeTestExecutionListener.class},
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public @interface SpringBatchTest {
}

View File

@@ -0,0 +1,4 @@
# Batch ContextCustomizerFactory implementation for the Spring TestContext Framework
#
org.springframework.test.context.ContextCustomizerFactory= \
org.springframework.batch.test.context.BatchTestContextCustomizerFactory

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test;
import java.util.Arrays;
import javax.sql.DataSource;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.batch.test.context.SpringBatchTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Test cases for usage of {@link SpringBatchTest} annotation.
*
* @author Mahmoud Ben Hassine
*/
@RunWith(SpringRunner.class)
@SpringBatchTest
@ContextConfiguration(classes = SpringBatchTestTests.JobConfiguration.class)
public class SpringBatchTestTests {
@Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
@Autowired
private JobRepositoryTestUtils jobRepositoryTestUtils;
@Autowired
private ItemReader<String> stepScopedItemReader;
@Autowired
private ItemReader<String> jobScopedItemReader;
@Before
public void setUp() {
this.jobRepositoryTestUtils.removeJobExecutions();
}
public StepExecution getStepExecution() {
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
execution.getExecutionContext().putString("input.data", "foo,bar");
return execution;
}
public JobExecution getJobExecution() {
JobExecution execution = MetaDataInstanceFactory.createJobExecution();
execution.getExecutionContext().putString("input.data", "foo,bar");
return execution;
}
@Test
public void testStepScopedItemReader() throws Exception {
Assert.assertEquals("foo", this.stepScopedItemReader.read());
Assert.assertEquals("bar", this.stepScopedItemReader.read());
Assert.assertNull(this.stepScopedItemReader.read());
}
@Test
public void testJobScopedItemReader() throws Exception {
Assert.assertEquals("foo", this.jobScopedItemReader.read());
Assert.assertEquals("bar", this.jobScopedItemReader.read());
Assert.assertNull(this.jobScopedItemReader.read());
}
@Test
public void testJob() throws Exception {
// when
JobExecution jobExecution = this.jobLauncherTestUtils.launchJob();
// then
Assert.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
}
@Configuration
@EnableBatchProcessing
public static class JobConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
.build();
}
@Bean
@StepScope
public ItemReader<String> stepScopedItemReader(@Value("#{stepExecutionContext['input.data']}") String data) {
return new ListItemReader<>(Arrays.asList(data.split(",")));
}
@Bean
@JobScope
public ItemReader<String> jobScopedItemReader(@Value("#{jobExecutionContext['input.data']}") String data) {
return new ListItemReader<>(Arrays.asList(data.split(",")));
}
@Bean
public Job job() {
return this.jobBuilderFactory.get("job")
.start(this.stepBuilderFactory.get("step")
.tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED)
.build())
.build();
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test.context;
import java.util.Collections;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
/**
* @author Mahmoud Ben Hassine
*/
public class BatchTestContextCustomizerFactoryTest {
private BatchTestContextCustomizerFactory factory = new BatchTestContextCustomizerFactory();
@Test
public void testCreateContextCustomizer_whenAnnotationIsPresent() {
// given
Class<MyJobTest> testClass = MyJobTest.class;
List<ContextConfigurationAttributes> configAttributes = Collections.emptyList();
// when
ContextCustomizer contextCustomizer = this.factory.createContextCustomizer(testClass, configAttributes);
// then
Assert.assertNotNull(contextCustomizer);
}
@Test
public void testCreateContextCustomizer_whenAnnotationIsAbsent() {
// given
Class<MyOtherJobTest> testClass = MyOtherJobTest.class;
List<ContextConfigurationAttributes> configAttributes = Collections.emptyList();
// when
ContextCustomizer contextCustomizer = this.factory.createContextCustomizer(testClass, configAttributes);
// then
Assert.assertNull(contextCustomizer);
}
@SpringBatchTest
private static class MyJobTest {
}
private static class MyOtherJobTest {
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.test.context;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.MergedContextConfiguration;
/**
* @author Mahmoud Ben Hassine
*/
public class BatchTestContextCustomizerTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
private BatchTestContextCustomizer contextCustomizer = new BatchTestContextCustomizer();
@Test
public void testCustomizeContext() {
// given
ConfigurableApplicationContext context = new GenericApplicationContext();
MergedContextConfiguration mergedConfig = Mockito.mock(MergedContextConfiguration.class);
// when
this.contextCustomizer.customizeContext(context, mergedConfig);
// then
Assert.assertTrue(context.containsBean("jobLauncherTestUtils"));
Assert.assertTrue(context.containsBean("jobRepositoryTestUtils"));
}
@Test
public void testCustomizeContext_whenBeanFactoryIsNotAnInstanceOfBeanDefinitionRegistry() {
// given
ConfigurableApplicationContext context = Mockito.mock(ConfigurableApplicationContext.class);
MergedContextConfiguration mergedConfig = Mockito.mock(MergedContextConfiguration.class);
this.expectedException.expect(IllegalArgumentException.class);
this.expectedException.expectMessage("The bean factory must be an instance of BeanDefinitionRegistry");
// when
this.contextCustomizer.customizeContext(context, mergedConfig);
// then
// expected exception
}
}