001/*
002 * Copyright 2006-2010 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.JobExecution;
021import org.springframework.batch.core.scope.context.JobContext;
022import org.springframework.batch.core.scope.context.JobSynchronizationManager;
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 job-scope context for
031 * dependency injection into unit tests. A {@link JobContext} 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 JobExecution} with fixed properties. Alternatively it
034 * 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, JobScopeTestExecutionListener.class })
040 * &#064;RunWith(SpringJUnit4ClassRunner.class)
041 * public class JobScopeTestExecutionListenerIntegrationTests {
042 * 
043 *      // A job-scoped dependency configured in the ApplicationContext
044 *      &#064;Autowired
045 *      private ItemReader&lt;String&gt; reader;
046 * 
047 *      public JobExecution getJobExecution() {
048 *              JobExecution execution = MetaDataInstanceFactory.createJobExecution();
049 *              execution.getExecutionContext().putString(&quot;foo&quot;, &quot;bar&quot;);
050 *              return execution;
051 *      }
052 * 
053 *      &#064;Test
054 *      public void testJobScopedReader() {
055 *              // Job context is active here so the reader can be used,
056 *              // and the job execution context will contain foo=bar...
057 *              assertNotNull(reader.read());
058 *      }
059 * 
060 * }
061 * </pre>
062 * 
063 * @author Dave Syer
064 * @author Jimmy Praet
065 */
066public class JobScopeTestExecutionListener implements TestExecutionListener {
067
068        private static final String JOB_EXECUTION = JobScopeTestExecutionListener.class.getName() + ".JOB_EXECUTION";
069
070        /**
071         * Set up a {@link JobExecution} as a test context attribute.
072         * 
073         * @param testContext the current test context
074         * @throws Exception if there is a problem
075         * @see TestExecutionListener#prepareTestInstance(TestContext)
076         */
077        @Override
078        public void prepareTestInstance(TestContext testContext) throws Exception {
079                JobExecution jobExecution = getJobExecution(testContext);
080                if (jobExecution != null) {
081                        testContext.setAttribute(JOB_EXECUTION, jobExecution);
082                }
083        }
084
085        /**
086         * @param testContext the current test context
087         * @throws Exception if there is a problem
088         * @see TestExecutionListener#beforeTestMethod(TestContext)
089         */
090        @Override
091        public void beforeTestMethod(org.springframework.test.context.TestContext testContext) throws Exception {
092                if (testContext.hasAttribute(JOB_EXECUTION)) {
093                        JobExecution jobExecution = (JobExecution) testContext.getAttribute(JOB_EXECUTION);
094                        JobSynchronizationManager.register(jobExecution);
095                }
096
097        }
098
099        /**
100         * @param testContext the current test context
101         * @throws Exception if there is a problem
102         * @see TestExecutionListener#afterTestMethod(TestContext)
103         */
104        @Override
105        public void afterTestMethod(TestContext testContext) throws Exception {
106                if (testContext.hasAttribute(JOB_EXECUTION)) {
107                        JobSynchronizationManager.close();
108                }
109        }
110
111        /*
112         * Support for Spring 3.0 (empty).
113         */
114        @Override
115        public void afterTestClass(TestContext testContext) throws Exception {
116        }
117
118        /*
119         * Support for Spring 3.0 (empty).
120         */
121        @Override
122        public void beforeTestClass(TestContext testContext) throws Exception {
123        }
124        
125        /**
126         * Discover a {@link JobExecution} as a field in the test case or create
127         * one if none is available.
128         * 
129         * @param testContext the current test context
130         * @return a {@link JobExecution}
131         */
132        protected JobExecution getJobExecution(TestContext testContext) {
133
134                Object target = testContext.getTestInstance();
135
136                ExtractorMethodCallback method = new ExtractorMethodCallback(JobExecution.class, "getJobExecution");
137                ReflectionUtils.doWithMethods(target.getClass(), method);
138                if (method.getName() != null) {
139                        HippyMethodInvoker invoker = new HippyMethodInvoker();
140                        invoker.setTargetObject(target);
141                        invoker.setTargetMethod(method.getName());
142                        try {
143                                invoker.prepare();
144                                return (JobExecution) invoker.invoke();
145                        }
146                        catch (Exception e) {
147                                throw new IllegalArgumentException("Could not create job execution from method: " + method.getName(),
148                                                e);
149                        }
150                }
151
152                return MetaDataInstanceFactory.createJobExecution();
153        }
154
155        /**
156         * Look for a method returning the type provided, preferring one with the
157         * name provided.
158         */
159        private final class ExtractorMethodCallback implements MethodCallback {
160                private String preferredName;
161
162                private final Class<?> preferredType;
163
164                private Method result;
165
166                public ExtractorMethodCallback(Class<?> preferredType, String preferredName) {
167                        super();
168                        this.preferredType = preferredType;
169                        this.preferredName = preferredName;
170                }
171
172                public String getName() {
173                        return result == null ? null : result.getName();
174                }
175
176                @Override
177                public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
178                        Class<?> type = method.getReturnType();
179                        if (preferredType.isAssignableFrom(type)) {
180                                if (result == null || method.getName().equals(preferredName)) {
181                                        result = method;
182                                }
183                        }
184                }
185        }
186
187}