001package com.unitils.boot.util;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import org.junit.Ignore;
006import org.junit.Test;
007import org.junit.internal.runners.model.ReflectiveCallable;
008import org.junit.internal.runners.statements.ExpectException;
009import org.junit.internal.runners.statements.Fail;
010import org.junit.runner.Description;
011import org.junit.runner.notification.RunNotifier;
012import org.junit.runners.BlockJUnit4ClassRunner;
013import org.junit.runners.model.FrameworkMethod;
014import org.junit.runners.model.InitializationError;
015import org.junit.runners.model.Statement;
016import org.springframework.test.annotation.ProfileValueUtils;
017import org.springframework.test.annotation.TestAnnotationUtils;
018import org.springframework.test.context.TestContextManager;
019import org.springframework.test.context.junit4.rules.SpringClassRule;
020import org.springframework.test.context.junit4.rules.SpringMethodRule;
021import org.springframework.test.context.junit4.statements.*;
022import org.springframework.util.ClassUtils;
023import org.springframework.util.ReflectionUtils;
024import org.unitils.core.TestListener;
025import org.unitils.core.Unitils;
026import org.unitils.core.junit.*;
027
028import java.lang.reflect.Field;
029import java.lang.reflect.Method;
030
031public class UnitilsBootBlockJUnit4ClassRunner extends BlockJUnit4ClassRunner {
032
033
034    private static final Log logger = LogFactory.getLog(UnitilsBootBlockJUnit4ClassRunner.class);
035
036    private static final Method withRulesMethod;
037
038    protected Object test;
039
040    protected TestListener unitilsTestListener;
041
042    static {
043        if (!ClassUtils.isPresent("org.junit.internal.Throwables", UnitilsBootBlockJUnit4ClassRunner.class.getClassLoader())) {
044            throw new IllegalStateException("UnitilsBootBlockJUnit4ClassRunner requires JUnit 4.12 or higher.");
045        }
046
047        withRulesMethod = ReflectionUtils.findMethod(UnitilsBootBlockJUnit4ClassRunner.class, "withRules",
048                FrameworkMethod.class, Object.class, Statement.class);
049        if (withRulesMethod == null) {
050            throw new IllegalStateException("UnitilsBootBlockJUnit4ClassRunner requires JUnit 4.12 or higher.");
051        }
052        ReflectionUtils.makeAccessible(withRulesMethod);
053    }
054
055
056    private final TestContextManager testContextManager;
057
058
059    private static void ensureSpringRulesAreNotPresent(Class<?> testClass) {
060        for (Field field : testClass.getFields()) {
061            if (SpringClassRule.class.isAssignableFrom(field.getType())) {
062                throw new IllegalStateException(String.format("Detected SpringClassRule field in test class [%s], " +
063                        "but SpringClassRule cannot be used with the UnitilsBootBlockJUnit4ClassRunner.", testClass.getName()));
064            }
065            if (SpringMethodRule.class.isAssignableFrom(field.getType())) {
066                throw new IllegalStateException(String.format("Detected SpringMethodRule field in test class [%s], " +
067                        "but SpringMethodRule cannot be used with the UnitilsBootBlockJUnit4ClassRunner.", testClass.getName()));
068            }
069        }
070    }
071
072    /**
073     * Construct a new {@code UnitilsBootBlockJUnit4ClassRunner} and initialize a
074     * {@link TestContextManager} to provide Spring testing functionality to
075     * standard JUnit tests.
076     * @param clazz the test class to be run
077     * @see #createTestContextManager(Class)
078     */
079    public UnitilsBootBlockJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
080        super(clazz);
081        if (logger.isDebugEnabled()) {
082            logger.debug("UnitilsBootBlockJUnit4ClassRunner constructor called with [" + clazz + "]");
083        }
084        ensureSpringRulesAreNotPresent(clazz);
085        this.testContextManager = createTestContextManager(clazz);
086        this.unitilsTestListener = getUnitilsTestListener();
087    }
088
089    protected TestListener getUnitilsTestListener() {
090        return Unitils.getInstance().getTestListener();
091    }
092
093    @Override
094    protected Statement methodInvoker(FrameworkMethod method, Object test) {
095        this.test = test;
096        Statement statement = super.methodInvoker(method, test);
097        statement = new BeforeTestMethodStatement(unitilsTestListener, statement, method.getMethod(), test);
098        statement = new AfterTestMethodStatement(unitilsTestListener, statement, method.getMethod(), test);
099        return statement;
100    }
101
102    @Override
103    protected Statement classBlock(RunNotifier notifier) {
104        Class<?> testClass = getTestClass().getJavaClass();
105
106        Statement statement = super.classBlock(notifier);
107        statement = new BeforeTestClassStatement(testClass, unitilsTestListener, statement);
108        return statement;
109    }
110
111    /**
112     * Create a new {@link TestContextManager} for the supplied test class.
113     * <p>Can be overridden by subclasses.
114     * @param clazz the test class to be managed
115     */
116    protected TestContextManager createTestContextManager(Class<?> clazz) {
117        return new TestContextManager(clazz);
118    }
119
120    /**
121     * Get the {@link TestContextManager} associated with this runner.
122     */
123    protected final TestContextManager getTestContextManager() {
124        return this.testContextManager;
125    }
126
127    /**
128     * Return a description suitable for an ignored test class if the test is
129     * disabled via {@code @IfProfileValue} at the class-level, and
130     * otherwise delegate to the parent implementation.
131     * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
132     */
133    @Override
134    public Description getDescription() {
135        if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
136            return Description.createSuiteDescription(getTestClass().getJavaClass());
137        }
138        return super.getDescription();
139    }
140
141    /**
142     * Check whether the test is enabled in the current execution environment.
143     * <p>This prevents classes with a non-matching {@code @IfProfileValue}
144     * annotation from running altogether, even skipping the execution of
145     * {@code prepareTestInstance()} methods in {@code TestExecutionListeners}.
146     * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
147     * @see org.springframework.test.annotation.IfProfileValue
148     * @see org.springframework.test.context.TestExecutionListener
149     */
150    @Override
151    public void run(RunNotifier notifier) {
152        if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
153            notifier.fireTestIgnored(getDescription());
154            return;
155        }
156        super.run(notifier);
157    }
158
159    /**
160     * Wrap the {@link Statement} returned by the parent implementation with a
161     * {@code RunBeforeTestClassCallbacks} statement, thus preserving the
162     * default JUnit functionality while adding support for the Spring TestContext
163     * Framework.
164     * @see RunBeforeTestClassCallbacks
165     */
166    @Override
167    protected Statement withBeforeClasses(Statement statement) {
168        Statement junitBeforeClasses = super.withBeforeClasses(statement);
169        return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());
170    }
171
172    /**
173     * Wrap the {@link Statement} returned by the parent implementation with a
174     * {@code RunAfterTestClassCallbacks} statement, thus preserving the default
175     * JUnit functionality while adding support for the Spring TestContext Framework.
176     * @see RunAfterTestClassCallbacks
177     */
178    @Override
179    protected Statement withAfterClasses(Statement statement) {
180        Statement junitAfterClasses = super.withAfterClasses(statement);
181        return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());
182    }
183
184    /**
185     * Delegate to the parent implementation for creating the test instance and
186     * then allow the {@link #getTestContextManager() TestContextManager} to
187     * prepare the test instance before returning it.
188     * @see TestContextManager#prepareTestInstance
189     */
190    @Override
191    protected Object createTest() throws Exception {
192        Object testInstance = super.createTest();
193        getTestContextManager().prepareTestInstance(testInstance);
194        return testInstance;
195    }
196
197    /**
198     * Perform the same logic as
199     * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},
200     * except that tests are determined to be <em>ignored</em> by
201     * {@link #isTestMethodIgnored(FrameworkMethod)}.
202     */
203    @Override
204    protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) {
205        Description description = describeChild(frameworkMethod);
206        if (isTestMethodIgnored(frameworkMethod)) {
207            notifier.fireTestIgnored(description);
208        }
209        else {
210            Statement statement;
211            try {
212                statement = methodBlock(frameworkMethod);
213            }
214            catch (Throwable ex) {
215                statement = new Fail(ex);
216            }
217            runLeaf(statement, description, notifier);
218        }
219    }
220
221    /**
222     * Augment the default JUnit behavior
223     * {@linkplain #withPotentialRepeat with potential repeats} of the entire
224     * execution chain.
225     * <p>Furthermore, support for timeouts has been moved down the execution
226     * chain in order to include execution of {@link org.junit.Before @Before}
227     * and {@link org.junit.After @After} methods within the timed execution.
228     * Note that this differs from the default JUnit behavior of executing
229     * {@code @Before} and {@code @After} methods in the main thread while
230     * executing the actual test method in a separate thread. Thus, the net
231     * effect is that {@code @Before} and {@code @After} methods will be
232     * executed in the same thread as the test method. As a consequence,
233     * JUnit-specified timeouts will work fine in combination with Spring
234     * transactions. However, JUnit-specific timeouts still differ from
235     * Spring-specific timeouts in that the former execute in a separate
236     * thread while the latter simply execute in the main thread (like regular
237     * tests).
238     * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement)
239     * @see #withBefores(FrameworkMethod, Object, Statement)
240     * @see #withAfters(FrameworkMethod, Object, Statement)
241     * @see #withRulesReflectively(FrameworkMethod, Object, Statement)
242     * @see #withPotentialRepeat(FrameworkMethod, Object, Statement)
243     * @see #withPotentialTimeout(FrameworkMethod, Object, Statement)
244     */
245    @Override
246    protected Statement methodBlock(FrameworkMethod frameworkMethod) {
247        Object testInstance;
248        try {
249            testInstance = new ReflectiveCallable() {
250                @Override
251                protected Object runReflectiveCall() throws Throwable {
252                    return createTest();
253                }
254            }.run();
255        }
256        catch (Throwable ex) {
257            return new Fail(ex);
258        }
259
260        Statement statement = methodInvoker(frameworkMethod, testInstance);
261        statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
262        statement = withBefores(frameworkMethod, testInstance, statement);
263        statement = withAfters(frameworkMethod, testInstance, statement);
264        statement = withRulesReflectively(frameworkMethod, testInstance, statement);
265        statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
266        statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
267        Method testMethod = frameworkMethod.getMethod();
268        statement = new BeforeTestSetUpStatement(test, testMethod, unitilsTestListener, statement);
269        statement = new AfterTestTearDownStatement(unitilsTestListener, statement, test, testMethod);
270        return statement;
271    }
272
273    /**
274     * Invoke JUnit's private {@code withRules()} method using reflection.
275     */
276    private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
277        return (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance, statement);
278    }
279
280    /**
281     * Return {@code true} if {@link Ignore @Ignore} is present for the supplied
282     * {@linkplain FrameworkMethod test method} or if the test method is disabled
283     * via {@code @IfProfileValue}.
284     * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class)
285     */
286    protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) {
287        Method method = frameworkMethod.getMethod();
288        return (method.isAnnotationPresent(Ignore.class) ||
289                !ProfileValueUtils.isTestEnabledInThisEnvironment(method, getTestClass().getJavaClass()));
290    }
291
292    /**
293     * Perform the same logic as
294     * {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)}
295     * except that the <em>expected exception</em> is retrieved using
296     * {@link #getExpectedException(FrameworkMethod)}.
297     */
298    @Override
299    protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
300        Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod);
301        return (expectedException != null ? new ExpectException(next, expectedException) : next);
302    }
303
304    /**
305     * Get the {@code exception} that the supplied {@linkplain FrameworkMethod
306     * test method} is expected to throw.
307     * <p>Supports JUnit's {@link Test#expected() @Test(expected=...)} annotation.
308     * <p>Can be overridden by subclasses.
309     * @return the expected exception, or {@code null} if none was specified
310     */
311    protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) {
312        Test test = frameworkMethod.getAnnotation(Test.class);
313        return (test != null && test.expected() != Test.None.class ? test.expected() : null);
314    }
315
316    /**
317     * Wrap the {@link Statement} returned by the parent implementation with a
318     * {@code RunBeforeTestMethodCallbacks} statement, thus preserving the
319     * default functionality while adding support for the Spring TestContext
320     * Framework.
321     * @see RunBeforeTestMethodCallbacks
322     */
323    @Override
324    protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
325        Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement);
326        return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(),
327                getTestContextManager());
328    }
329
330    /**
331     * Wrap the {@link Statement} returned by the parent implementation with a
332     * {@code RunAfterTestMethodCallbacks} statement, thus preserving the
333     * default functionality while adding support for the Spring TestContext
334     * Framework.
335     * @see RunAfterTestMethodCallbacks
336     */
337    @Override
338    protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
339        Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement);
340        return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(),
341                getTestContextManager());
342    }
343
344    /**
345     * Wrap the supplied {@link Statement} with a {@code SpringRepeat} statement.
346     * <p>Supports Spring's {@link org.springframework.test.annotation.Repeat @Repeat}
347     * annotation.
348     * @see TestAnnotationUtils#getRepeatCount(Method)
349     * @see SpringRepeat
350     */
351    protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
352        return new SpringRepeat(next, frameworkMethod.getMethod());
353    }
354
355
356}