diff --git a/spring-batch-test/.springBeans b/spring-batch-test/.springBeans index caea10aa6..4f4cc5522 100644 --- a/spring-batch-test/.springBeans +++ b/spring-batch-test/.springBeans @@ -1,7 +1,7 @@ 1 - + @@ -14,6 +14,7 @@ src/test/resources/jobs/sampleSimpleJob.xml src/test/resources/jobs/sample-steps.xml src/test/resources/job-runner-context.xml + src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests-context.xml diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestExecutionListener.java b/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestExecutionListener.java new file mode 100644 index 000000000..798407282 --- /dev/null +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestExecutionListener.java @@ -0,0 +1,276 @@ +/* + * Copyright 2006-2010 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.lang.reflect.Field; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.scope.context.StepContext; +import org.springframework.batch.core.scope.context.StepSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; + +/** + * A {@link TestExecutionListener} that sets up step-scope context for + * dependency injection into unit tests. A {@link StepContext} will be created + * for the duration of a test method and made available to any dependencies that + * are injected. The default behaviour is just to create a {@link JobExecution} + * and {@link StepExecution} with fixed properties. Alternatively they can be + * provided by the test case as a field of the correct type. If those fields are + * not provided then an {@link ExecutionContext} for the default step execution + * can be specified as a field of type ExecutionContext, or a field of type Map + * (those fields can have any name but to disambiguate you can use the special + * name "executionContext". And finally, {@link JobParameters} can be specified + * using the same convention: a field of that type or a Map (with the field name + * "jobParameters" used to disambiguate). Example: + * + *
+ * @ContextConfiguration
+ * @TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class })
+ * @RunWith(SpringJUnit4ClassRunner.class)
+ * public class StepScopeTestExecutionListenerIntegrationTests {
+ * 
+ * 	// A step-scoped dependency configured in the ApplicationContext
+ * 	@Autowired
+ * 	private ItemReader<String> reader;
+ * 
+ * 	@Test
+ * 	public void testStepScopedReader() {
+ * 		// Step context is active here so the reader can be used...
+ * 		assertNotNull(reader.read());
+ * 	}
+ * 
+ * }
+ * 
+ * + * @author Dave Syer + * + */ +public class StepScopeTestExecutionListener implements TestExecutionListener { + + private static final String STEP_EXECUTION = StepScopeTestExecutionListener.class.getName() + ".STEP_EXECUTION"; + + /** + * Set up a {@link StepExecution} as a test context attribute. + * + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#prepareTestInstance(TestContext) + */ + public void prepareTestInstance(TestContext testContext) throws Exception { + StepExecution stepExecution = getStepExecution(testContext); + if (stepExecution != null) { + testContext.setAttribute(STEP_EXECUTION, stepExecution); + } + } + + /** + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#beforeTestMethod(TestContext) + */ + public void beforeTestMethod(org.springframework.test.context.TestContext testContext) throws Exception { + if (testContext.hasAttribute(STEP_EXECUTION)) { + StepExecution stepExecution = (StepExecution) testContext.getAttribute(STEP_EXECUTION); + StepSynchronizationManager.register(stepExecution); + } + + } + + /** + * @param testContext the current test context + * @throws Exception if there is a problem + * @see TestExecutionListener#afterTestMethod(TestContext) + */ + public void afterTestMethod(TestContext testContext) throws Exception { + if (testContext.hasAttribute(STEP_EXECUTION)) { + StepSynchronizationManager.close(); + } + } + + /** + * Discover a {@link StepExecution} as a field in the test case or create + * one if none is available. + * + * @param testContext the current test context + * @return a {@link StepExecution} + */ + protected StepExecution getStepExecution(TestContext testContext) { + + Object target = testContext.getTestInstance(); + + ExtractorFieldCallback extractor = new ExtractorFieldCallback(StepExecution.class, "stepExecution"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + if (extractor.getName() != null) { + return (StepExecution) ReflectionTestUtils.getField(target, extractor.getName()); + } + + StepExecution stepExecution = null; + + extractor = new ExtractorFieldCallback(Map.class, "executionContext"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + + Map map = null; + + if (extractor.getName() == null) { + extractor = new ExtractorFieldCallback(ExecutionContext.class, "executionContext"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + if (extractor.getName() != null) { + map = new HashMap(); + ExecutionContext executionContext = ((ExecutionContext) ReflectionTestUtils.getField(target, extractor + .getName())); + for (Entry entry : executionContext.entrySet()) { + map.put(entry.getKey(), entry.getValue()); + } + } + } + else { + @SuppressWarnings("unchecked") + Map themap = (Map) ReflectionTestUtils + .getField(target, extractor.getName()); + map = themap; + } + + JobExecution jobExecution = getJobExecution(testContext); + if (map == null) { + map = new HashMap(); + } + if (stepExecution == null) { + if (jobExecution != null) { + stepExecution = jobExecution.createStepExecution("step"); + } + else { + stepExecution = MetaDataInstanceFactory.createStepExecution(); + } + } + for (String key : map.keySet()) { + stepExecution.getExecutionContext().put(key, map.get(key)); + } + + return stepExecution; + + } + + /** + * Discover a {@link JobExecution} as a field in the test case or create + * one if none is available. + * + * @param testContext the current test context + * @return a {@link JobExecution} + */ + private JobExecution getJobExecution(TestContext testContext) { + + Object target = testContext.getTestInstance(); + + ExtractorFieldCallback extractor = new ExtractorFieldCallback(JobExecution.class, "jobExecution"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + if (extractor.getName() != null) { + return (JobExecution) ReflectionTestUtils.getField(target, extractor.getName()); + } + + extractor = new ExtractorFieldCallback(Map.class, "jobParameters"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + + Map map = null; + + if (extractor.getName() == null) { + extractor = new ExtractorFieldCallback(JobParameters.class, "jobParameters"); + ReflectionUtils.doWithFields(target.getClass(), extractor); + if (extractor.getName() != null) { + map = new HashMap(); + JobParameters jobParameters = ((JobParameters) ReflectionTestUtils + .getField(target, extractor.getName())); + for (Entry entry : jobParameters.getParameters().entrySet()) { + map.put(entry.getKey(), entry.getValue().getValue()); + } + } + } + else { + @SuppressWarnings("unchecked") + Map themap = (Map) ReflectionTestUtils + .getField(target, extractor.getName()); + map = themap; + } + + if (map != null) { + Map parameters = new HashMap(); + for (String key : map.keySet()) { + Object value = map.get(key); + if (value == null) { + parameters.put(key, new JobParameter((String) null)); + } + else if (value instanceof String) { + parameters.put(key, new JobParameter((String) value)); + } + else if (value instanceof Double) { + parameters.put(key, new JobParameter((Double) value)); + } + else if (value instanceof Long) { + parameters.put(key, new JobParameter((Long) value)); + } + else if (value instanceof Date) { + parameters.put(key, new JobParameter((Date) value)); + } + } + return MetaDataInstanceFactory.createJobExecution("job", 11L, 123L, new JobParameters(parameters)); + } + + return null; + + } + + /** + * Look for a Map in the fields provided, preferring one with the name + * provided. + */ + private final class ExtractorFieldCallback implements FieldCallback { + private String preferredName; + + private final Class preferredType; + + private Field result; + + public ExtractorFieldCallback(Class preferredType, String preferredName) { + super(); + this.preferredType = preferredType; + this.preferredName = preferredName; + } + + public String getName() { + return result == null ? null : result.getName(); + } + + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + Class type = field.getType(); + if (preferredType.isAssignableFrom(type)) { + if (result == null || field.getName().equals(preferredName)) { + result = field; + } + } + } + } +} diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests.java new file mode 100644 index 000000000..001f8e227 --- /dev/null +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests.java @@ -0,0 +1,47 @@ +package org.springframework.batch.test; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.ItemStream; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * @author Dave Syer + * @since 2.1 + */ +@ContextConfiguration +@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class }) +@RunWith(SpringJUnit4ClassRunner.class) +public class StepScopeTestExecutionListenerIntegrationTests { + + @Autowired + private ItemReader reader; + + @Autowired + private ItemStream stream; + + protected Map executionContext; + + public StepScopeTestExecutionListenerIntegrationTests() { + executionContext = Collections.singletonMap("input.file", + (Object) "classpath:/org/springframework/batch/test/simple.txt"); + } + + @Test + public void testJob() throws Exception { + stream.open(new ExecutionContext()); + assertEquals("foo", reader.read()); + } + +} diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerTests.java new file mode 100644 index 000000000..4d89e04b1 --- /dev/null +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeTestExecutionListenerTests.java @@ -0,0 +1,188 @@ +package org.springframework.batch.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Collections; +import java.util.Map; + +import org.junit.Test; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.scope.context.StepContext; +import org.springframework.batch.core.scope.context.StepSynchronizationManager; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextManager; + +/** + * @author Dave Syer + * @since 2.1 + */ +@ContextConfiguration +public class StepScopeTestExecutionListenerTests { + + private StepScopeTestExecutionListener listener = new StepScopeTestExecutionListener(); + + @Test + public void testStepContext() throws Exception { + TestContext testContext = getTestContext(new Object()); + listener.prepareTestInstance(testContext); + listener.beforeTestMethod(testContext); + StepContext context = StepSynchronizationManager.getContext(); + assertNotNull(context); + listener.afterTestMethod(testContext); + assertNull(StepSynchronizationManager.getContext()); + } + + @Test + public void testWithStepExecution() throws Exception { + testExecutionContext(new WithStepExecution()); + } + + @Test + public void testWithMapForContext() throws Exception { + testExecutionContext(new WithMap()); + } + + @Test + public void testWithTwoMapForContext() throws Exception { + testExecutionContext(new WithTwoMaps()); + } + + @Test + public void testWithContextForContext() throws Exception { + testExecutionContext(new WithExecutionContext()); + } + + @Test + public void testWithTwpContextsForContext() throws Exception { + testExecutionContext(new WithTwoMaps()); + } + + @Test + public void testWithJobExecution() throws Exception { + testJobParameters(new WithJobExecution()); + } + + @Test + public void testWithMapForParameters() throws Exception { + testJobParameters(new WithMap()); + } + + @Test + public void testWithTwoMapsForParameters() throws Exception { + testJobParameters(new WithParametersMap()); + } + + @Test + public void testWithParameters() throws Exception { + testJobParameters(new WithParameters()); + } + + @Test + public void testWithTwoParameters() throws Exception { + testJobParameters(new WithTwoParameters()); + } + + private void testExecutionContext(Object target) throws Exception { + TestContext testContext = getTestContext(target); + listener.prepareTestInstance(testContext); + listener.beforeTestMethod(testContext); + StepContext context = StepSynchronizationManager.getContext(); + assertNotNull(context); + assertEquals("bar", context.getStepExecutionContext().get("foo")); + listener.afterTestMethod(testContext); + assertNull(StepSynchronizationManager.getContext()); + } + + private void testJobParameters(Object target) throws Exception { + TestContext testContext = getTestContext(target); + listener.prepareTestInstance(testContext); + listener.beforeTestMethod(testContext); + StepContext context = StepSynchronizationManager.getContext(); + assertNotNull(context); + assertEquals("bar", context.getJobParameters().get("foo")); + listener.afterTestMethod(testContext); + assertNull(StepSynchronizationManager.getContext()); + } + + private static class WithStepExecution { + private StepExecution execution = MetaDataInstanceFactory.createStepExecution(); + + public WithStepExecution() { + execution.getExecutionContext().putString("foo", "bar"); + } + } + + @SuppressWarnings("unused") + private static class WithJobExecution { + private JobExecution execution = MetaDataInstanceFactory.createJobExecution("job", 11L, 123L, new JobParametersBuilder().addString("foo", "bar").toJobParameters()); + } + + @SuppressWarnings("unused") + private static class WithMap { + private Map context = Collections.singletonMap("foo", (Object) "bar"); + } + + @SuppressWarnings("unused") + private static class WithTwoMaps { + private Map executionContext = Collections.singletonMap("foo", (Object) "bar"); + + private Map map = Collections.singletonMap("foo", (Object) "spam"); + } + + @SuppressWarnings("unused") + private static class WithParametersMap { + private Map jobParameters = Collections.singletonMap("foo", (Object) "bar"); + + private Map map = Collections.singletonMap("foo", (Object) "spam"); + } + + @SuppressWarnings("unused") + private static class WithExecutionContext { + private ExecutionContext context = new ExecutionContext(Collections.singletonMap("foo", (Object) "bar")); + } + + @SuppressWarnings("unused") + public static class WithTwoExecutionContextw { + private ExecutionContext executionContext = new ExecutionContext(Collections + .singletonMap("foo", (Object) "bar")); + + private ExecutionContext context = new ExecutionContext(Collections.singletonMap("foo", (Object) "spam")); + } + + @SuppressWarnings("unused") + private static class WithParameters { + private JobParameters params = new JobParametersBuilder().addString("foo", "bar").toJobParameters(); + } + + @SuppressWarnings("unused") + private static class WithTwoParameters { + private JobParameters jobParemeters = new JobParametersBuilder().addString("foo", "bar").toJobParameters(); + + private JobParameters params = new JobParametersBuilder().addString("foo", "spam").toJobParameters(); + } + + private TestContext getTestContext(Object target) throws Exception { + return new MockTestContextManager(target, getClass()).getContext(); + } + + private final class MockTestContextManager extends TestContextManager { + + private MockTestContextManager(Object target, Class testClass) throws Exception { + super(testClass); + prepareTestInstance(target); + } + + public TestContext getContext() { + return getTestContext(); + } + + } + +} diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests-context.xml new file mode 100644 index 000000000..b5c08bd4d --- /dev/null +++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerIntegrationTests-context.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerTests-context.xml new file mode 100644 index 000000000..59fcf8914 --- /dev/null +++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/StepScopeTestExecutionListenerTests-context.xml @@ -0,0 +1,5 @@ + + + + diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/simple.txt b/spring-batch-test/src/test/resources/org/springframework/batch/test/simple.txt new file mode 100644 index 000000000..3bd1f0e29 --- /dev/null +++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/simple.txt @@ -0,0 +1,2 @@ +foo +bar