001/*
002 * Copyright 2002-2016 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 */
016
017package org.springframework.test.context.junit4;
018
019import java.lang.reflect.Field;
020import java.lang.reflect.Method;
021import java.util.concurrent.TimeUnit;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025
026import org.junit.Ignore;
027import org.junit.Test;
028import org.junit.internal.runners.model.ReflectiveCallable;
029import org.junit.internal.runners.statements.ExpectException;
030import org.junit.internal.runners.statements.Fail;
031import org.junit.internal.runners.statements.FailOnTimeout;
032import org.junit.runner.Description;
033import org.junit.runner.notification.RunNotifier;
034import org.junit.runners.BlockJUnit4ClassRunner;
035import org.junit.runners.model.FrameworkMethod;
036import org.junit.runners.model.InitializationError;
037import org.junit.runners.model.Statement;
038
039import org.springframework.test.annotation.ProfileValueUtils;
040import org.springframework.test.annotation.TestAnnotationUtils;
041import org.springframework.test.context.TestContextManager;
042import org.springframework.test.context.junit4.rules.SpringClassRule;
043import org.springframework.test.context.junit4.rules.SpringMethodRule;
044import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks;
045import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks;
046import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks;
047import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks;
048import org.springframework.test.context.junit4.statements.SpringFailOnTimeout;
049import org.springframework.test.context.junit4.statements.SpringRepeat;
050import org.springframework.util.ClassUtils;
051import org.springframework.util.ReflectionUtils;
052
053/**
054 * {@code SpringJUnit4ClassRunner} is a custom extension of JUnit's
055 * {@link BlockJUnit4ClassRunner} which provides functionality of the
056 * <em>Spring TestContext Framework</em> to standard JUnit tests by means of the
057 * {@link TestContextManager} and associated support classes and annotations.
058 *
059 * <p>To use this class, simply annotate a JUnit 4 based test class with
060 * {@code @RunWith(SpringJUnit4ClassRunner.class)} or {@code @RunWith(SpringRunner.class)}.
061 *
062 * <p>The following list constitutes all annotations currently supported directly
063 * or indirectly by {@code SpringJUnit4ClassRunner}. <em>(Note that additional
064 * annotations may be supported by various
065 * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}
066 * or {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
067 * implementations.)</em>
068 *
069 * <ul>
070 * <li>{@link Test#expected() @Test(expected=...)}</li>
071 * <li>{@link Test#timeout() @Test(timeout=...)}</li>
072 * <li>{@link org.springframework.test.annotation.Timed @Timed}</li>
073 * <li>{@link org.springframework.test.annotation.Repeat @Repeat}</li>
074 * <li>{@link Ignore @Ignore}</li>
075 * <li>{@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li>
076 * <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li>
077 * </ul>
078 *
079 * <p>If you would like to use the Spring TestContext Framework with a runner
080 * other than this one, use {@link SpringClassRule} and {@link SpringMethodRule}.
081 *
082 * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher.
083 *
084 * @author Sam Brannen
085 * @author Juergen Hoeller
086 * @since 2.5
087 * @see SpringRunner
088 * @see TestContextManager
089 * @see AbstractJUnit4SpringContextTests
090 * @see AbstractTransactionalJUnit4SpringContextTests
091 * @see org.springframework.test.context.junit4.rules.SpringClassRule
092 * @see org.springframework.test.context.junit4.rules.SpringMethodRule
093 */
094public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
095
096        private static final Log logger = LogFactory.getLog(SpringJUnit4ClassRunner.class);
097
098        private static final Method withRulesMethod;
099
100        static {
101                if (!ClassUtils.isPresent("org.junit.internal.Throwables", SpringJUnit4ClassRunner.class.getClassLoader())) {
102                        throw new IllegalStateException("SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
103                }
104
105                withRulesMethod = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules",
106                                FrameworkMethod.class, Object.class, Statement.class);
107                if (withRulesMethod == null) {
108                        throw new IllegalStateException("SpringJUnit4ClassRunner requires JUnit 4.12 or higher.");
109                }
110                ReflectionUtils.makeAccessible(withRulesMethod);
111        }
112
113
114        private final TestContextManager testContextManager;
115
116
117        private static void ensureSpringRulesAreNotPresent(Class<?> testClass) {
118                for (Field field : testClass.getFields()) {
119                        if (SpringClassRule.class.isAssignableFrom(field.getType())) {
120                                throw new IllegalStateException(String.format("Detected SpringClassRule field in test class [%s], " +
121                                                "but SpringClassRule cannot be used with the SpringJUnit4ClassRunner.", testClass.getName()));
122                        }
123                        if (SpringMethodRule.class.isAssignableFrom(field.getType())) {
124                                throw new IllegalStateException(String.format("Detected SpringMethodRule field in test class [%s], " +
125                                                "but SpringMethodRule cannot be used with the SpringJUnit4ClassRunner.", testClass.getName()));
126                        }
127                }
128        }
129
130        /**
131         * Construct a new {@code SpringJUnit4ClassRunner} and initialize a
132         * {@link TestContextManager} to provide Spring testing functionality to
133         * standard JUnit tests.
134         * @param clazz the test class to be run
135         * @see #createTestContextManager(Class)
136         */
137        public SpringJUnit4ClassRunner(Class<?> clazz) throws InitializationError {
138                super(clazz);
139                if (logger.isDebugEnabled()) {
140                        logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
141                }
142                ensureSpringRulesAreNotPresent(clazz);
143                this.testContextManager = createTestContextManager(clazz);
144        }
145
146        /**
147         * Create a new {@link TestContextManager} for the supplied test class.
148         * <p>Can be overridden by subclasses.
149         * @param clazz the test class to be managed
150         */
151        protected TestContextManager createTestContextManager(Class<?> clazz) {
152                return new TestContextManager(clazz);
153        }
154
155        /**
156         * Get the {@link TestContextManager} associated with this runner.
157         */
158        protected final TestContextManager getTestContextManager() {
159                return this.testContextManager;
160        }
161
162        /**
163         * Return a description suitable for an ignored test class if the test is
164         * disabled via {@code @IfProfileValue} at the class-level, and
165         * otherwise delegate to the parent implementation.
166         * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
167         */
168        @Override
169        public Description getDescription() {
170                if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
171                        return Description.createSuiteDescription(getTestClass().getJavaClass());
172                }
173                return super.getDescription();
174        }
175
176        /**
177         * Check whether the test is enabled in the current execution environment.
178         * <p>This prevents classes with a non-matching {@code @IfProfileValue}
179         * annotation from running altogether, even skipping the execution of
180         * {@code prepareTestInstance()} methods in {@code TestExecutionListeners}.
181         * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class)
182         * @see org.springframework.test.annotation.IfProfileValue
183         * @see org.springframework.test.context.TestExecutionListener
184         */
185        @Override
186        public void run(RunNotifier notifier) {
187                if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
188                        notifier.fireTestIgnored(getDescription());
189                        return;
190                }
191                super.run(notifier);
192        }
193
194        /**
195         * Wrap the {@link Statement} returned by the parent implementation with a
196         * {@code RunBeforeTestClassCallbacks} statement, thus preserving the
197         * default JUnit functionality while adding support for the Spring TestContext
198         * Framework.
199         * @see RunBeforeTestClassCallbacks
200         */
201        @Override
202        protected Statement withBeforeClasses(Statement statement) {
203                Statement junitBeforeClasses = super.withBeforeClasses(statement);
204                return new RunBeforeTestClassCallbacks(junitBeforeClasses, getTestContextManager());
205        }
206
207        /**
208         * Wrap the {@link Statement} returned by the parent implementation with a
209         * {@code RunAfterTestClassCallbacks} statement, thus preserving the default
210         * JUnit functionality while adding support for the Spring TestContext Framework.
211         * @see RunAfterTestClassCallbacks
212         */
213        @Override
214        protected Statement withAfterClasses(Statement statement) {
215                Statement junitAfterClasses = super.withAfterClasses(statement);
216                return new RunAfterTestClassCallbacks(junitAfterClasses, getTestContextManager());
217        }
218
219        /**
220         * Delegate to the parent implementation for creating the test instance and
221         * then allow the {@link #getTestContextManager() TestContextManager} to
222         * prepare the test instance before returning it.
223         * @see TestContextManager#prepareTestInstance
224         */
225        @Override
226        protected Object createTest() throws Exception {
227                Object testInstance = super.createTest();
228                getTestContextManager().prepareTestInstance(testInstance);
229                return testInstance;
230        }
231
232        /**
233         * Perform the same logic as
234         * {@link BlockJUnit4ClassRunner#runChild(FrameworkMethod, RunNotifier)},
235         * except that tests are determined to be <em>ignored</em> by
236         * {@link #isTestMethodIgnored(FrameworkMethod)}.
237         */
238        @Override
239        protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) {
240                Description description = describeChild(frameworkMethod);
241                if (isTestMethodIgnored(frameworkMethod)) {
242                        notifier.fireTestIgnored(description);
243                }
244                else {
245                        Statement statement;
246                        try {
247                                statement = methodBlock(frameworkMethod);
248                        }
249                        catch (Throwable ex) {
250                                statement = new Fail(ex);
251                        }
252                        runLeaf(statement, description, notifier);
253                }
254        }
255
256        /**
257         * Augment the default JUnit behavior
258         * {@linkplain #withPotentialRepeat with potential repeats} of the entire
259         * execution chain.
260         * <p>Furthermore, support for timeouts has been moved down the execution
261         * chain in order to include execution of {@link org.junit.Before @Before}
262         * and {@link org.junit.After @After} methods within the timed execution.
263         * Note that this differs from the default JUnit behavior of executing
264         * {@code @Before} and {@code @After} methods in the main thread while
265         * executing the actual test method in a separate thread. Thus, the net
266         * effect is that {@code @Before} and {@code @After} methods will be
267         * executed in the same thread as the test method. As a consequence,
268         * JUnit-specified timeouts will work fine in combination with Spring
269         * transactions. However, JUnit-specific timeouts still differ from
270         * Spring-specific timeouts in that the former execute in a separate
271         * thread while the latter simply execute in the main thread (like regular
272         * tests).
273         * @see #possiblyExpectingExceptions(FrameworkMethod, Object, Statement)
274         * @see #withBefores(FrameworkMethod, Object, Statement)
275         * @see #withAfters(FrameworkMethod, Object, Statement)
276         * @see #withRulesReflectively(FrameworkMethod, Object, Statement)
277         * @see #withPotentialRepeat(FrameworkMethod, Object, Statement)
278         * @see #withPotentialTimeout(FrameworkMethod, Object, Statement)
279         */
280        @Override
281        protected Statement methodBlock(FrameworkMethod frameworkMethod) {
282                Object testInstance;
283                try {
284                        testInstance = new ReflectiveCallable() {
285                                @Override
286                                protected Object runReflectiveCall() throws Throwable {
287                                        return createTest();
288                                }
289                        }.run();
290                }
291                catch (Throwable ex) {
292                        return new Fail(ex);
293                }
294
295                Statement statement = methodInvoker(frameworkMethod, testInstance);
296                statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement);
297                statement = withBefores(frameworkMethod, testInstance, statement);
298                statement = withAfters(frameworkMethod, testInstance, statement);
299                statement = withRulesReflectively(frameworkMethod, testInstance, statement);
300                statement = withPotentialRepeat(frameworkMethod, testInstance, statement);
301                statement = withPotentialTimeout(frameworkMethod, testInstance, statement);
302                return statement;
303        }
304
305        /**
306         * Invoke JUnit's private {@code withRules()} method using reflection.
307         */
308        private Statement withRulesReflectively(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
309                return (Statement) ReflectionUtils.invokeMethod(withRulesMethod, this, frameworkMethod, testInstance, statement);
310        }
311
312        /**
313         * Return {@code true} if {@link Ignore @Ignore} is present for the supplied
314         * {@linkplain FrameworkMethod test method} or if the test method is disabled
315         * via {@code @IfProfileValue}.
316         * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class)
317         */
318        protected boolean isTestMethodIgnored(FrameworkMethod frameworkMethod) {
319                Method method = frameworkMethod.getMethod();
320                return (method.isAnnotationPresent(Ignore.class) ||
321                                !ProfileValueUtils.isTestEnabledInThisEnvironment(method, getTestClass().getJavaClass()));
322        }
323
324        /**
325         * Perform the same logic as
326         * {@link BlockJUnit4ClassRunner#possiblyExpectingExceptions(FrameworkMethod, Object, Statement)}
327         * except that the <em>expected exception</em> is retrieved using
328         * {@link #getExpectedException(FrameworkMethod)}.
329         */
330        @Override
331        protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
332                Class<? extends Throwable> expectedException = getExpectedException(frameworkMethod);
333                return (expectedException != null ? new ExpectException(next, expectedException) : next);
334        }
335
336        /**
337         * Get the {@code exception} that the supplied {@linkplain FrameworkMethod
338         * test method} is expected to throw.
339         * <p>Supports JUnit's {@link Test#expected() @Test(expected=...)} annotation.
340         * <p>Can be overridden by subclasses.
341         * @return the expected exception, or {@code null} if none was specified
342         */
343        protected Class<? extends Throwable> getExpectedException(FrameworkMethod frameworkMethod) {
344                Test test = frameworkMethod.getAnnotation(Test.class);
345                return (test != null && test.expected() != Test.None.class ? test.expected() : null);
346        }
347
348        /**
349         * Perform the same logic as
350         * {@link BlockJUnit4ClassRunner#withPotentialTimeout(FrameworkMethod, Object, Statement)}
351         * but with additional support for Spring's {@code @Timed} annotation.
352         * <p>Supports both Spring's {@link org.springframework.test.annotation.Timed @Timed}
353         * and JUnit's {@link Test#timeout() @Test(timeout=...)} annotations, but not both
354         * simultaneously.
355         * @return either a {@link SpringFailOnTimeout}, a {@link FailOnTimeout},
356         * or the supplied {@link Statement} as appropriate
357         * @see #getSpringTimeout(FrameworkMethod)
358         * @see #getJUnitTimeout(FrameworkMethod)
359         */
360        @Override
361        @SuppressWarnings("deprecation")
362        protected Statement withPotentialTimeout(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
363                Statement statement = null;
364                long springTimeout = getSpringTimeout(frameworkMethod);
365                long junitTimeout = getJUnitTimeout(frameworkMethod);
366                if (springTimeout > 0 && junitTimeout > 0) {
367                        String msg = String.format("Test method [%s] has been configured with Spring's @Timed(millis=%s) and " +
368                                                        "JUnit's @Test(timeout=%s) annotations, but only one declaration of a 'timeout' is " +
369                                                        "permitted per test method.", frameworkMethod.getMethod(), springTimeout, junitTimeout);
370                        logger.error(msg);
371                        throw new IllegalStateException(msg);
372                }
373                else if (springTimeout > 0) {
374                        statement = new SpringFailOnTimeout(next, springTimeout);
375                }
376                else if (junitTimeout > 0) {
377                        statement = FailOnTimeout.builder().withTimeout(junitTimeout, TimeUnit.MILLISECONDS).build(next);
378                }
379                else {
380                        statement = next;
381                }
382
383                return statement;
384        }
385
386        /**
387         * Retrieve the configured JUnit {@code timeout} from the {@link Test @Test}
388         * annotation on the supplied {@linkplain FrameworkMethod test method}.
389         * @return the timeout, or {@code 0} if none was specified
390         */
391        protected long getJUnitTimeout(FrameworkMethod frameworkMethod) {
392                Test test = frameworkMethod.getAnnotation(Test.class);
393                return (test != null && test.timeout() > 0 ? test.timeout() : 0);
394        }
395
396        /**
397         * Retrieve the configured Spring-specific {@code timeout} from the
398         * {@link org.springframework.test.annotation.Timed @Timed} annotation
399         * on the supplied {@linkplain FrameworkMethod test method}.
400         * @return the timeout, or {@code 0} if none was specified
401         * @see TestAnnotationUtils#getTimeout(Method)
402         */
403        protected long getSpringTimeout(FrameworkMethod frameworkMethod) {
404                return TestAnnotationUtils.getTimeout(frameworkMethod.getMethod());
405        }
406
407        /**
408         * Wrap the {@link Statement} returned by the parent implementation with a
409         * {@code RunBeforeTestMethodCallbacks} statement, thus preserving the
410         * default functionality while adding support for the Spring TestContext
411         * Framework.
412         * @see RunBeforeTestMethodCallbacks
413         */
414        @Override
415        protected Statement withBefores(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
416                Statement junitBefores = super.withBefores(frameworkMethod, testInstance, statement);
417                return new RunBeforeTestMethodCallbacks(junitBefores, testInstance, frameworkMethod.getMethod(),
418                                getTestContextManager());
419        }
420
421        /**
422         * Wrap the {@link Statement} returned by the parent implementation with a
423         * {@code RunAfterTestMethodCallbacks} statement, thus preserving the
424         * default functionality while adding support for the Spring TestContext
425         * Framework.
426         * @see RunAfterTestMethodCallbacks
427         */
428        @Override
429        protected Statement withAfters(FrameworkMethod frameworkMethod, Object testInstance, Statement statement) {
430                Statement junitAfters = super.withAfters(frameworkMethod, testInstance, statement);
431                return new RunAfterTestMethodCallbacks(junitAfters, testInstance, frameworkMethod.getMethod(),
432                                getTestContextManager());
433        }
434
435        /**
436         * Wrap the supplied {@link Statement} with a {@code SpringRepeat} statement.
437         * <p>Supports Spring's {@link org.springframework.test.annotation.Repeat @Repeat}
438         * annotation.
439         * @see TestAnnotationUtils#getRepeatCount(Method)
440         * @see SpringRepeat
441         */
442        protected Statement withPotentialRepeat(FrameworkMethod frameworkMethod, Object testInstance, Statement next) {
443                return new SpringRepeat(next, frameworkMethod.getMethod());
444        }
445
446}