001/* 002 * Copyright 2006-2014 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.springframework.batch.test; 017 018import java.lang.reflect.Method; 019 020import org.springframework.batch.core.StepExecution; 021import org.springframework.batch.core.scope.context.StepContext; 022import org.springframework.batch.core.scope.context.StepSynchronizationManager; 023import org.springframework.batch.item.adapter.HippyMethodInvoker; 024import org.springframework.test.context.TestContext; 025import org.springframework.test.context.TestExecutionListener; 026import org.springframework.util.ReflectionUtils; 027import org.springframework.util.ReflectionUtils.MethodCallback; 028 029/** 030 * A {@link TestExecutionListener} that sets up step-scope context for 031 * dependency injection into unit tests. A {@link StepContext} will be created 032 * for the duration of a test method and made available to any dependencies that 033 * are injected. The default behaviour is just to create a {@link StepExecution} 034 * with fixed properties. Alternatively it can be provided by the test case as a 035 * factory methods returning the correct type. Example: 036 * 037 * <pre> 038 * @ContextConfiguration 039 * @TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class }) 040 * @RunWith(SpringJUnit4ClassRunner.class) 041 * public class StepScopeTestExecutionListenerIntegrationTests { 042 * 043 * // A step-scoped dependency configured in the ApplicationContext 044 * @Autowired 045 * private ItemReader<String> reader; 046 * 047 * public StepExecution getStepExecution() { 048 * StepExecution execution = MetaDataInstanceFactory.createStepExecution(); 049 * execution.getExecutionContext().putString("foo", "bar"); 050 * return execution; 051 * } 052 * 053 * @Test 054 * public void testStepScopedReader() { 055 * // Step context is active here so the reader can be used, 056 * // and the step execution context will contain foo=bar... 057 * assertNotNull(reader.read()); 058 * } 059 * 060 * } 061 * </pre> 062 * 063 * @author Dave Syer 064 * @author Chris Schaefer 065 */ 066public class StepScopeTestExecutionListener implements TestExecutionListener { 067 private static final String STEP_EXECUTION = StepScopeTestExecutionListener.class.getName() + ".STEP_EXECUTION"; 068 private static final String SET_ATTRIBUTE_METHOD_NAME = "setAttribute"; 069 private static final String HAS_ATTRIBUTE_METHOD_NAME = "hasAttribute"; 070 private static final String GET_ATTRIBUTE_METHOD_NAME = "getAttribute"; 071 private static final String GET_TEST_INSTANCE_METHOD = "getTestInstance"; 072 073 /** 074 * Set up a {@link StepExecution} as a test context attribute. 075 * 076 * @param testContext the current test context 077 * @throws Exception if there is a problem 078 * @see TestExecutionListener#prepareTestInstance(TestContext) 079 */ 080 @Override 081 public void prepareTestInstance(TestContext testContext) throws Exception { 082 StepExecution stepExecution = getStepExecution(testContext); 083 084 if (stepExecution != null) { 085 Method method = TestContext.class.getMethod(SET_ATTRIBUTE_METHOD_NAME, String.class, Object.class); 086 ReflectionUtils.invokeMethod(method, testContext, STEP_EXECUTION, stepExecution); 087 } 088 } 089 090 /** 091 * @param testContext the current test context 092 * @throws Exception if there is a problem 093 * @see TestExecutionListener#beforeTestMethod(TestContext) 094 */ 095 @Override 096 public void beforeTestMethod(TestContext testContext) throws Exception { 097 Method hasAttributeMethod = TestContext.class.getMethod(HAS_ATTRIBUTE_METHOD_NAME, String.class); 098 Boolean hasAttribute = (Boolean) ReflectionUtils.invokeMethod(hasAttributeMethod, testContext, STEP_EXECUTION); 099 100 if (hasAttribute) { 101 Method method = TestContext.class.getMethod(GET_ATTRIBUTE_METHOD_NAME, String.class); 102 StepExecution stepExecution = (StepExecution) ReflectionUtils.invokeMethod(method, testContext, STEP_EXECUTION); 103 104 StepSynchronizationManager.register(stepExecution); 105 } 106 } 107 108 /** 109 * @param testContext the current test context 110 * @throws Exception if there is a problem 111 * @see TestExecutionListener#afterTestMethod(TestContext) 112 */ 113 @Override 114 public void afterTestMethod(TestContext testContext) throws Exception { 115 Method method = TestContext.class.getMethod(HAS_ATTRIBUTE_METHOD_NAME, String.class); 116 Boolean hasAttribute = (Boolean) ReflectionUtils.invokeMethod(method, testContext, STEP_EXECUTION); 117 118 if (hasAttribute) { 119 StepSynchronizationManager.close(); 120 } 121 } 122 123 /* 124 * Support for Spring 3.0 (empty). 125 */ 126 @Override 127 public void afterTestClass(TestContext testContext) throws Exception { 128 } 129 130 /* 131 * Support for Spring 3.0 (empty). 132 */ 133 @Override 134 public void beforeTestClass(TestContext testContext) throws Exception { 135 } 136 137 /** 138 * Discover a {@link StepExecution} as a field in the test case or create 139 * one if none is available. 140 * 141 * @param testContext the current test context 142 * @return a {@link StepExecution} 143 */ 144 protected StepExecution getStepExecution(TestContext testContext) { 145 Object target; 146 147 try { 148 Method method = TestContext.class.getMethod(GET_TEST_INSTANCE_METHOD); 149 target = ReflectionUtils.invokeMethod(method, testContext); 150 } catch (NoSuchMethodException e) { 151 throw new IllegalStateException("No such method " + GET_TEST_INSTANCE_METHOD + " on provided TestContext", e); 152 } 153 154 ExtractorMethodCallback method = new ExtractorMethodCallback(StepExecution.class, "getStepExecution"); 155 ReflectionUtils.doWithMethods(target.getClass(), method); 156 if (method.getName() != null) { 157 HippyMethodInvoker invoker = new HippyMethodInvoker(); 158 invoker.setTargetObject(target); 159 invoker.setTargetMethod(method.getName()); 160 try { 161 invoker.prepare(); 162 return (StepExecution) invoker.invoke(); 163 } 164 catch (Exception e) { 165 throw new IllegalArgumentException("Could not create step execution from method: " + method.getName(), 166 e); 167 } 168 } 169 170 return MetaDataInstanceFactory.createStepExecution(); 171 } 172 173 /** 174 * Look for a method returning the type provided, preferring one with the 175 * name provided. 176 */ 177 private final class ExtractorMethodCallback implements MethodCallback { 178 private String preferredName; 179 180 private final Class<?> preferredType; 181 182 private Method result; 183 184 public ExtractorMethodCallback(Class<?> preferredType, String preferredName) { 185 super(); 186 this.preferredType = preferredType; 187 this.preferredName = preferredName; 188 } 189 190 public String getName() { 191 return result == null ? null : result.getName(); 192 } 193 194 @Override 195 public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { 196 Class<?> type = method.getReturnType(); 197 if (preferredType.isAssignableFrom(type)) { 198 if (result == null || method.getName().equals(preferredName)) { 199 result = method; 200 } 201 } 202 } 203 } 204}