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 * &#064;ContextConfiguration
039 * &#064;TestExecutionListeners( { DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class })
040 * &#064;RunWith(SpringJUnit4ClassRunner.class)
041 * public class StepScopeTestExecutionListenerIntegrationTests {
042 * 
043 *      // A step-scoped dependency configured in the ApplicationContext
044 *      &#064;Autowired
045 *      private ItemReader&lt;String&gt; reader;
046 * 
047 *  public StepExecution getStepExecution() {
048 *    StepExecution execution = MetaDataInstanceFactory.createStepExecution();
049 *    execution.getExecutionContext().putString("foo", "bar");
050 *    return execution;
051 *  }
052 * 
053 *      &#064;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}