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 * @ContextConfiguration 039 * @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, JobScopeTestExecutionListener.class }) 040 * @RunWith(SpringJUnit4ClassRunner.class) 041 * public class JobScopeTestExecutionListenerIntegrationTests { 042 * 043 * // A job-scoped dependency configured in the ApplicationContext 044 * @Autowired 045 * private ItemReader<String> reader; 046 * 047 * public JobExecution getJobExecution() { 048 * JobExecution execution = MetaDataInstanceFactory.createJobExecution(); 049 * execution.getExecutionContext().putString("foo", "bar"); 050 * return execution; 051 * } 052 * 053 * @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}