Skip to content

Commit 0dab67c

Browse files
fmbenhassinemminella
authored andcommitted
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
1 parent 98070e4 commit 0dab67c

File tree

10 files changed

+536
-9
lines changed

10 files changed

+536
-9
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ project('spring-batch-test') {
497497

498498
testCompile "org.apache.commons:commons-dbcp2:$commonsDdbcpVersion"
499499
testCompile "org.hsqldb:hsqldb:$hsqldbVersion"
500+
testCompile "org.mockito:mockito-core:$mockitoVersion"
500501

501502
optional "org.aspectj:aspectjrt:$aspectjVersion"
502503
optional "javax.batch:javax.batch-api:$javaxBatchApiVersion"

spring-batch-docs/asciidoc/testing.adoc

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,33 @@ In order for the unit test to run a batch job, the framework must
2828
this behavior:
2929

3030

31-
* `@RunWith(SpringJUnit4ClassRunner.class)`:
31+
* `@RunWith(SpringRunner.class)`:
3232
Indicates that the class should use Spring's JUnit facilities
3333

3434

3535
* `@ContextConfiguration(...)`:
3636
Indicates which resources to configure the `ApplicationContext` with.
3737

38-
The following example shows the two annotations in use:
38+
Starting from v4.1, it is also possible to inject Spring Batch test utilities
39+
like the `JobLauncherTestUtils` and `JobRepositoryTestUtils` in the test context
40+
using the `@SpringBatchTest` annotation.
41+
42+
The following example shows the annotations in use:
3943

4044
.Using Java Configuration
4145
[source, java, role="javaContent"]
4246
----
43-
@RunWith(SpringJUnit4ClassRunner.class)
47+
@SpringBatchTest
48+
@RunWith(SpringRunner.class)
4449
@ContextConfiguration(classes=SkipSampleConfiguration.class)
4550
public class SkipSampleFunctionalTests { ... }
4651
----
4752

4853
.Using XML Configuration
4954
[source, java, role="xmlContent"]
5055
----
51-
@RunWith(SpringJUnit4ClassRunner.class)
56+
@SpringBatchTest
57+
@RunWith(SpringRunner.class)
5258
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
5359
"/jobs/skipSampleJob.xml" })
5460
public class SkipSampleFunctionalTests { ... }
@@ -81,7 +87,8 @@ In the following example, the batch job reads from the database and
8187
.XML Based Configuration
8288
[source, java, role="xmlContent"]
8389
----
84-
@RunWith(SpringJUnit4ClassRunner.class)
90+
@SpringBatchTest
91+
@RunWith(SpringRunner.class)
8592
@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
8693
"/jobs/skipSampleJob.xml" })
8794
public class SkipSampleFunctionalTests {
@@ -115,7 +122,8 @@ public class SkipSampleFunctionalTests {
115122
.Java Based Configuration
116123
[source, java, role="javaContent"]
117124
----
118-
@RunWith(SpringJUnit4ClassRunner.class)
125+
@SpringBatchTest
126+
@RunWith(SpringRunner.class)
119127
@ContextConfiguration(classes=SkipSampleConfiguration.class)
120128
public class SkipSampleFunctionalTests {
121129
@@ -187,15 +195,15 @@ The listener is declared at the class level, and its job is to
187195
@ContextConfiguration
188196
@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
189197
StepScopeTestExecutionListener.class })
190-
@RunWith(SpringJUnit4ClassRunner.class)
198+
@RunWith(SpringRunner.class)
191199
public class StepScopeTestExecutionListenerIntegrationTests {
192200
193201
// This component is defined step-scoped, so it cannot be injected unless
194202
// a step is active...
195203
@Autowired
196204
private ItemReader<String> reader;
197205
198-
public StepExecution getStepExection() {
206+
public StepExecution getStepExecution() {
199207
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
200208
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
201209
return execution;
@@ -222,6 +230,38 @@ There are two `TestExecutionListeners`. One
222230
`StepExecution`). If a factory method is not provided,
223231
then a default `StepExecution` is created.
224232

233+
Starting from v4.1, the `StepScopeTestExecutionListener` and
234+
`JobScopeTestExecutionListener` are imported as test execution listeners
235+
if the test class is annotated with `@SpringBatchTest`. The preceding test
236+
example can be configured as follows:
237+
238+
[source, java]
239+
----
240+
@SpringBatchTest
241+
@RunWith(SpringRunner.class)
242+
@ContextConfiguration
243+
public class StepScopeTestExecutionListenerIntegrationTests {
244+
245+
// This component is defined step-scoped, so it cannot be injected unless
246+
// a step is active...
247+
@Autowired
248+
private ItemReader<String> reader;
249+
250+
public StepExecution getStepExecution() {
251+
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
252+
execution.getExecutionContext().putString("input.data", "foo,bar,spam");
253+
return execution;
254+
}
255+
256+
@Test
257+
public void testReader() {
258+
// The reader is initialized and bound to the input data
259+
assertNotNull(reader.read());
260+
}
261+
262+
}
263+
----
264+
225265
The listener approach is convenient if you want the duration of the
226266
step scope to be the execution of the test method. For a more flexible
227267
but more invasive approach, you can use the

spring-batch-test/src/main/java/org/springframework/batch/test/JobRepositoryTestUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2007 the original author or authors.
2+
* Copyright 2006-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
import org.springframework.batch.core.repository.JobRestartException;
3737
import org.springframework.batch.core.repository.dao.AbstractJdbcBatchMetadataDao;
3838
import org.springframework.beans.factory.InitializingBean;
39+
import org.springframework.beans.factory.annotation.Autowired;
3940
import org.springframework.dao.DataAccessException;
4041
import org.springframework.jdbc.core.JdbcOperations;
4142
import org.springframework.jdbc.core.JdbcTemplate;
@@ -49,6 +50,7 @@
4950
* the transaction.
5051
*
5152
* @author Dave Syer
53+
* @author Mahmoud Ben Hassine
5254
*/
5355
public class JobRepositoryTestUtils extends AbstractJdbcBatchMetadataDao implements InitializingBean {
5456

@@ -95,6 +97,7 @@ public JobRepositoryTestUtils(JobRepository jobRepository, DataSource dataSource
9597
setDataSource(dataSource);
9698
}
9799

100+
@Autowired
98101
public final void setDataSource(DataSource dataSource) {
99102
jdbcTemplate = new JdbcTemplate(dataSource);
100103
}
@@ -109,6 +112,7 @@ public void setJobParametersIncrementer(JobParametersIncrementer jobParametersIn
109112
/**
110113
* @param jobRepository the jobRepository to set
111114
*/
115+
@Autowired
112116
public void setJobRepository(JobRepository jobRepository) {
113117
this.jobRepository = jobRepository;
114118
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.test.context;
17+
18+
import org.springframework.batch.test.JobLauncherTestUtils;
19+
import org.springframework.batch.test.JobRepositoryTestUtils;
20+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
21+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
22+
import org.springframework.beans.factory.support.RootBeanDefinition;
23+
import org.springframework.context.ConfigurableApplicationContext;
24+
import org.springframework.test.context.ContextCustomizer;
25+
import org.springframework.test.context.MergedContextConfiguration;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* {@link ContextCustomizer} implementation that adds batch test utility classes
30+
* ({@link JobLauncherTestUtils} and {@link JobRepositoryTestUtils}) as beans in
31+
* the test context.
32+
*
33+
* @author Mahmoud Ben Hassine
34+
* @since 4.1
35+
*/
36+
public class BatchTestContextCustomizer implements ContextCustomizer {
37+
38+
private static final String JOB_LAUNCHER_TEST_UTILS_BEAN_NAME = "jobLauncherTestUtils";
39+
private static final String JOB_REPOSITORY_TEST_UTILS_BEAN_NAME = "jobRepositoryTestUtils";
40+
41+
@Override
42+
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
43+
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
44+
Assert.isInstanceOf(BeanDefinitionRegistry.class, beanFactory,
45+
"The bean factory must be an instance of BeanDefinitionRegistry");
46+
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
47+
48+
registry.registerBeanDefinition(JOB_LAUNCHER_TEST_UTILS_BEAN_NAME,
49+
new RootBeanDefinition(JobLauncherTestUtils.class));
50+
registry.registerBeanDefinition(JOB_REPOSITORY_TEST_UTILS_BEAN_NAME,
51+
new RootBeanDefinition(JobRepositoryTestUtils.class));
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.test.context;
17+
18+
import java.util.List;
19+
20+
import org.springframework.core.annotation.AnnotatedElementUtils;
21+
import org.springframework.test.context.ContextConfigurationAttributes;
22+
import org.springframework.test.context.ContextCustomizer;
23+
import org.springframework.test.context.ContextCustomizerFactory;
24+
25+
/**
26+
* Factory for {@link BatchTestContextCustomizer}.
27+
*
28+
* @author Mahmoud Ben Hassine
29+
* @since 4.1
30+
*/
31+
public class BatchTestContextCustomizerFactory implements ContextCustomizerFactory {
32+
33+
@Override
34+
public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
35+
SpringBatchTest springBatchTest = AnnotatedElementUtils.findMergedAnnotation(testClass, SpringBatchTest.class);
36+
if (springBatchTest != null) {
37+
return new BatchTestContextCustomizer();
38+
}
39+
return null;
40+
}
41+
42+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.test.context;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.batch.test.JobLauncherTestUtils;
26+
import org.springframework.batch.test.JobRepositoryTestUtils;
27+
import org.springframework.batch.test.JobScopeTestExecutionListener;
28+
import org.springframework.batch.test.StepScopeTestExecutionListener;
29+
import org.springframework.test.context.TestExecutionListeners;
30+
31+
/**
32+
* Annotation that can be specified on a test class that runs Spring Batch based tests.
33+
* Provides the following features over the regular <em>Spring TestContext Framework</em>:
34+
* <ul>
35+
* <li>Registers a {@link JobLauncherTestUtils} bean with the
36+
* {@link BatchTestContextCustomizer#JOB_LAUNCHER_TEST_UTILS_BEAN_NAME} which can be used
37+
* in tests for launching jobs and steps.
38+
* </li>
39+
* <li>Registers a {@link JobRepositoryTestUtils} bean
40+
* with the {@link BatchTestContextCustomizer#JOB_REPOSITORY_TEST_UTILS_BEAN_NAME}
41+
* which can be used in tests setup to create or remove job executions.
42+
* </li>
43+
* <li>Registers the {@link StepScopeTestExecutionListener} and {@link JobScopeTestExecutionListener}
44+
* as test execution listeners which are required to test step/job scoped beans.
45+
* </li>
46+
* </ul>
47+
* <p>
48+
* A typical usage of this annotation is like:
49+
*
50+
* <pre class="code">
51+
* &#064;RunWith(SpringRunner.class)
52+
* &#064;SpringBatchTest
53+
* &#064;ContextConfiguration(classes = MyBatchJobConfiguration.class)
54+
* public class MyBatchJobTests {
55+
*
56+
* &#064;@Autowired
57+
* private JobLauncherTestUtils jobLauncherTestUtils;
58+
*
59+
* &#064;@Autowired
60+
* private JobRepositoryTestUtils jobRepositoryTestUtils;
61+
*
62+
* &#064;Before
63+
* public void clearJobExecutions() {
64+
* this.jobRepositoryTestUtils.removeJobExecutions();
65+
* }
66+
*
67+
* &#064;Test
68+
* public void testMyJob() throws Exception {
69+
* // given
70+
* JobParameters jobParameters = this.jobLauncherTestUtils.getUniqueJobParameters();
71+
*
72+
* // when
73+
* JobExecution jobExecution = this.jobLauncherTestUtils.launchJob(jobParameters);
74+
*
75+
* // then
76+
* Assert.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
77+
* }
78+
*
79+
* }
80+
* </pre>
81+
*
82+
* @author Mahmoud Ben Hassine
83+
* @since 4.1
84+
* @see JobLauncherTestUtils
85+
* @see JobRepositoryTestUtils
86+
* @see StepScopeTestExecutionListener
87+
* @see JobScopeTestExecutionListener
88+
*/
89+
@Target(ElementType.TYPE)
90+
@Retention(RetentionPolicy.RUNTIME)
91+
@Documented
92+
@Inherited
93+
@TestExecutionListeners(
94+
listeners = {StepScopeTestExecutionListener.class, JobScopeTestExecutionListener.class},
95+
mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
96+
)
97+
public @interface SpringBatchTest {
98+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Batch ContextCustomizerFactory implementation for the Spring TestContext Framework
2+
#
3+
org.springframework.test.context.ContextCustomizerFactory= \
4+
org.springframework.batch.test.context.BatchTestContextCustomizerFactory

0 commit comments

Comments
 (0)