001/*
002 * Copyright 2002-2015 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.rules;
018
019import java.lang.reflect.Field;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023
024import org.junit.ClassRule;
025import org.junit.rules.MethodRule;
026import org.junit.runners.model.FrameworkMethod;
027import org.junit.runners.model.Statement;
028
029import org.springframework.test.context.TestContextManager;
030import org.springframework.test.context.junit4.statements.ProfileValueChecker;
031import org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks;
032import org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks;
033import org.springframework.test.context.junit4.statements.RunPrepareTestInstanceCallbacks;
034import org.springframework.test.context.junit4.statements.SpringFailOnTimeout;
035import org.springframework.test.context.junit4.statements.SpringRepeat;
036import org.springframework.util.ClassUtils;
037import org.springframework.util.ReflectionUtils;
038
039/**
040 * {@code SpringMethodRule} is a custom JUnit {@link MethodRule} that
041 * supports instance-level and method-level features of the
042 * <em>Spring TestContext Framework</em> in standard JUnit tests by means
043 * of the {@link TestContextManager} and associated support classes and
044 * annotations.
045 *
046 * <p>In contrast to the {@link org.springframework.test.context.junit4.SpringJUnit4ClassRunner
047 * SpringJUnit4ClassRunner}, Spring's rule-based JUnit support has the advantage
048 * that it is independent of any {@link org.junit.runner.Runner Runner} and
049 * can therefore be combined with existing alternative runners like JUnit's
050 * {@code Parameterized} or third-party runners such as the {@code MockitoJUnitRunner}.
051 *
052 * <p>In order to achieve the same functionality as the {@code SpringJUnit4ClassRunner},
053 * however, a {@code SpringMethodRule} must be combined with a {@link SpringClassRule},
054 * since {@code SpringMethodRule} only supports the instance-level and method-level
055 * features of the {@code SpringJUnit4ClassRunner}.
056 *
057 * <h3>Example Usage</h3>
058 * <pre><code> public class ExampleSpringIntegrationTest {
059 *
060 *    &#064;ClassRule
061 *    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
062 *
063 *    &#064;Rule
064 *    public final SpringMethodRule springMethodRule = new SpringMethodRule();
065 *
066 *    // ...
067 * }</code></pre>
068 *
069 * <p>The following list constitutes all annotations currently supported directly
070 * or indirectly by {@code SpringMethodRule}. <em>(Note that additional annotations
071 * may be supported by various
072 * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener} or
073 * {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
074 * implementations.)</em>
075 *
076 * <ul>
077 * <li>{@link org.springframework.test.annotation.Timed @Timed}</li>
078 * <li>{@link org.springframework.test.annotation.Repeat @Repeat}</li>
079 * <li>{@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li>
080 * <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li>
081 * </ul>
082 *
083 * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher.
084 *
085 * @author Sam Brannen
086 * @author Philippe Marschall
087 * @since 4.2
088 * @see #apply(Statement, FrameworkMethod, Object)
089 * @see SpringClassRule
090 * @see org.springframework.test.context.TestContextManager
091 * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
092 */
093public class SpringMethodRule implements MethodRule {
094
095        private static final Log logger = LogFactory.getLog(SpringMethodRule.class);
096
097        static {
098                if (!ClassUtils.isPresent("org.junit.internal.Throwables", SpringMethodRule.class.getClassLoader())) {
099                        throw new IllegalStateException("SpringMethodRule requires JUnit 4.12 or higher.");
100                }
101        }
102
103
104        /**
105         * Apply <em>instance-level</em> and <em>method-level</em> features of
106         * the <em>Spring TestContext Framework</em> to the supplied {@code base}
107         * statement.
108         * <p>Specifically, this method invokes the
109         * {@link TestContextManager#prepareTestInstance prepareTestInstance()},
110         * {@link TestContextManager#beforeTestMethod beforeTestMethod()}, and
111         * {@link TestContextManager#afterTestMethod afterTestMethod()} methods
112         * on the {@code TestContextManager}, potentially with Spring timeouts
113         * and repetitions.
114         * <p>In addition, this method checks whether the test is enabled in
115         * the current execution environment. This prevents methods with a
116         * non-matching {@code @IfProfileValue} annotation from running altogether,
117         * even skipping the execution of {@code prepareTestInstance()} methods
118         * in {@code TestExecutionListeners}.
119         * @param base the base {@code Statement} that this rule should be applied to
120         * @param frameworkMethod the method which is about to be invoked on the test instance
121         * @param testInstance the current test instance
122         * @return a statement that wraps the supplied {@code base} with instance-level
123         * and method-level features of the Spring TestContext Framework
124         * @see #withBeforeTestMethodCallbacks
125         * @see #withAfterTestMethodCallbacks
126         * @see #withPotentialRepeat
127         * @see #withPotentialTimeout
128         * @see #withTestInstancePreparation
129         * @see #withProfileValueCheck
130         */
131        @Override
132        public Statement apply(Statement base, FrameworkMethod frameworkMethod, Object testInstance) {
133                if (logger.isDebugEnabled()) {
134                        logger.debug("Applying SpringMethodRule to test method [" + frameworkMethod.getMethod() + "]");
135                }
136                Class<?> testClass = testInstance.getClass();
137                validateSpringClassRuleConfiguration(testClass);
138                TestContextManager testContextManager = SpringClassRule.getTestContextManager(testClass);
139
140                Statement statement = base;
141                statement = withBeforeTestMethodCallbacks(statement, frameworkMethod, testInstance, testContextManager);
142                statement = withAfterTestMethodCallbacks(statement, frameworkMethod, testInstance, testContextManager);
143                statement = withTestInstancePreparation(statement, testInstance, testContextManager);
144                statement = withPotentialRepeat(statement, frameworkMethod, testInstance);
145                statement = withPotentialTimeout(statement, frameworkMethod, testInstance);
146                statement = withProfileValueCheck(statement, frameworkMethod, testInstance);
147                return statement;
148        }
149
150        /**
151         * Wrap the supplied {@link Statement} with a {@code RunBeforeTestMethodCallbacks} statement.
152         * @see RunBeforeTestMethodCallbacks
153         */
154        private Statement withBeforeTestMethodCallbacks(Statement statement, FrameworkMethod frameworkMethod,
155                        Object testInstance, TestContextManager testContextManager) {
156
157                return new RunBeforeTestMethodCallbacks(
158                                statement, testInstance, frameworkMethod.getMethod(), testContextManager);
159        }
160
161        /**
162         * Wrap the supplied {@link Statement} with a {@code RunAfterTestMethodCallbacks} statement.
163         * @see RunAfterTestMethodCallbacks
164         */
165        private Statement withAfterTestMethodCallbacks(Statement statement, FrameworkMethod frameworkMethod,
166                        Object testInstance, TestContextManager testContextManager) {
167
168                return new RunAfterTestMethodCallbacks(
169                                statement, testInstance, frameworkMethod.getMethod(), testContextManager);
170        }
171
172        /**
173         * Wrap the supplied {@link Statement} with a {@code RunPrepareTestInstanceCallbacks} statement.
174         * @see RunPrepareTestInstanceCallbacks
175         */
176        private Statement withTestInstancePreparation(Statement statement, Object testInstance,
177                        TestContextManager testContextManager) {
178
179                return new RunPrepareTestInstanceCallbacks(statement, testInstance, testContextManager);
180        }
181
182        /**
183         * Wrap the supplied {@link Statement} with a {@code SpringRepeat} statement.
184         * <p>Supports Spring's {@link org.springframework.test.annotation.Repeat @Repeat}
185         * annotation.
186         * @see SpringRepeat
187         */
188        private Statement withPotentialRepeat(Statement next, FrameworkMethod frameworkMethod, Object testInstance) {
189                return new SpringRepeat(next, frameworkMethod.getMethod());
190        }
191
192        /**
193         * Wrap the supplied {@link Statement} with a {@code SpringFailOnTimeout} statement.
194         * <p>Supports Spring's {@link org.springframework.test.annotation.Timed @Timed}
195         * annotation.
196         * @see SpringFailOnTimeout
197         */
198        private Statement withPotentialTimeout(Statement next, FrameworkMethod frameworkMethod, Object testInstance) {
199                return new SpringFailOnTimeout(next, frameworkMethod.getMethod());
200        }
201
202        /**
203         * Wrap the supplied {@link Statement} with a {@code ProfileValueChecker} statement.
204         * @see ProfileValueChecker
205         */
206        private Statement withProfileValueCheck(Statement statement, FrameworkMethod frameworkMethod, Object testInstance) {
207                return new ProfileValueChecker(statement, testInstance.getClass(), frameworkMethod.getMethod());
208        }
209
210
211        /**
212         * Throw an {@link IllegalStateException} if the supplied {@code testClass}
213         * does not declare a {@code public static final SpringClassRule} field
214         * that is annotated with {@code @ClassRule}.
215         */
216        private static SpringClassRule validateSpringClassRuleConfiguration(Class<?> testClass) {
217                Field ruleField = null;
218
219                for (Field field : testClass.getFields()) {
220                        if (ReflectionUtils.isPublicStaticFinal(field) && SpringClassRule.class.isAssignableFrom(field.getType())) {
221                                ruleField = field;
222                                break;
223                        }
224                }
225
226                if (ruleField == null) {
227                        throw new IllegalStateException(String.format(
228                                        "Failed to find 'public static final SpringClassRule' field in test class [%s]. " +
229                                        "Consult the javadoc for SpringClassRule for details.", testClass.getName()));
230                }
231
232                if (!ruleField.isAnnotationPresent(ClassRule.class)) {
233                        throw new IllegalStateException(String.format(
234                                        "SpringClassRule field [%s] must be annotated with JUnit's @ClassRule annotation. " +
235                                        "Consult the javadoc for SpringClassRule for details.", ruleField));
236                }
237
238                return (SpringClassRule) ReflectionUtils.getField(ruleField, null);
239        }
240
241}