001/*
002 * Copyright 2002-2018 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.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.junit.rules.TestRule;
025import org.junit.runner.Description;
026import org.junit.runners.model.Statement;
027
028import org.springframework.test.context.TestContextManager;
029import org.springframework.test.context.junit4.statements.ProfileValueChecker;
030import org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks;
031import org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks;
032import org.springframework.util.Assert;
033
034/**
035 * {@code SpringClassRule} is a custom JUnit {@link TestRule} that supports
036 * <em>class-level</em> features of the <em>Spring TestContext Framework</em>
037 * in standard JUnit tests by means of the {@link TestContextManager} and
038 * associated support classes and annotations.
039 *
040 * <p>In contrast to the {@link org.springframework.test.context.junit4.SpringJUnit4ClassRunner
041 * SpringJUnit4ClassRunner}, Spring's rule-based JUnit support has the advantage
042 * that it is independent of any {@link org.junit.runner.Runner Runner} and
043 * can therefore be combined with existing alternative runners like JUnit's
044 * {@code Parameterized} or third-party runners such as the {@code MockitoJUnitRunner}.
045 *
046 * <p>In order to achieve the same functionality as the {@code SpringJUnit4ClassRunner},
047 * however, a {@code SpringClassRule} must be combined with a {@link SpringMethodRule},
048 * since {@code SpringClassRule} only supports the class-level features of the
049 * {@code SpringJUnit4ClassRunner}.
050 *
051 * <h3>Example Usage</h3>
052 * <pre><code> public class ExampleSpringIntegrationTest {
053 *
054 *    &#064;ClassRule
055 *    public static final SpringClassRule springClassRule = new SpringClassRule();
056 *
057 *    &#064;Rule
058 *    public final SpringMethodRule springMethodRule = new SpringMethodRule();
059 *
060 *    // ...
061 * }</code></pre>
062 *
063 * <p>The following list constitutes all annotations currently supported directly
064 * or indirectly by {@code SpringClassRule}. <em>(Note that additional annotations
065 * may be supported by various
066 * {@link org.springframework.test.context.TestExecutionListener TestExecutionListener} or
067 * {@link org.springframework.test.context.TestContextBootstrapper TestContextBootstrapper}
068 * implementations.)</em>
069 *
070 * <ul>
071 * <li>{@link org.springframework.test.annotation.ProfileValueSourceConfiguration @ProfileValueSourceConfiguration}</li>
072 * <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li>
073 * </ul>
074 *
075 * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher.
076 *
077 * @author Sam Brannen
078 * @author Philippe Marschall
079 * @since 4.2
080 * @see #apply(Statement, Description)
081 * @see SpringMethodRule
082 * @see org.springframework.test.context.TestContextManager
083 * @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
084 */
085public class SpringClassRule implements TestRule {
086
087        private static final Log logger = LogFactory.getLog(SpringClassRule.class);
088
089        /**
090         * Cache of {@code TestContextManagers} keyed by test class.
091         */
092        private static final Map<Class<?>, TestContextManager> testContextManagerCache = new ConcurrentHashMap<>(64);
093
094
095        /**
096         * Apply <em>class-level</em> features of the <em>Spring TestContext
097         * Framework</em> to the supplied {@code base} statement.
098         * <p>Specifically, this method retrieves the {@link TestContextManager}
099         * used by this rule and its associated {@link SpringMethodRule} and
100         * invokes the {@link TestContextManager#beforeTestClass() beforeTestClass()}
101         * and {@link TestContextManager#afterTestClass() afterTestClass()} methods
102         * on the {@code TestContextManager}.
103         * <p>In addition, this method checks whether the test is enabled in
104         * the current execution environment. This prevents classes with a
105         * non-matching {@code @IfProfileValue} annotation from running altogether,
106         * even skipping the execution of {@code beforeTestClass()} methods
107         * in {@code TestExecutionListeners}.
108         * @param base the base {@code Statement} that this rule should be applied to
109         * @param description a {@code Description} of the current test execution
110         * @return a statement that wraps the supplied {@code base} with class-level
111         * features of the Spring TestContext Framework
112         * @see #getTestContextManager
113         * @see #withBeforeTestClassCallbacks
114         * @see #withAfterTestClassCallbacks
115         * @see #withProfileValueCheck
116         * @see #withTestContextManagerCacheEviction
117         */
118        @Override
119        public Statement apply(Statement base, Description description) {
120                Class<?> testClass = description.getTestClass();
121                if (logger.isDebugEnabled()) {
122                        logger.debug("Applying SpringClassRule to test class [" + testClass.getName() + "]");
123                }
124                TestContextManager testContextManager = getTestContextManager(testClass);
125
126                Statement statement = base;
127                statement = withBeforeTestClassCallbacks(statement, testContextManager);
128                statement = withAfterTestClassCallbacks(statement, testContextManager);
129                statement = withProfileValueCheck(statement, testClass);
130                statement = withTestContextManagerCacheEviction(statement, testClass);
131                return statement;
132        }
133
134        /**
135         * Wrap the supplied {@link Statement} with a {@code RunBeforeTestClassCallbacks} statement.
136         * @see RunBeforeTestClassCallbacks
137         */
138        private Statement withBeforeTestClassCallbacks(Statement next, TestContextManager testContextManager) {
139                return new RunBeforeTestClassCallbacks(next, testContextManager);
140        }
141
142        /**
143         * Wrap the supplied {@link Statement} with a {@code RunAfterTestClassCallbacks} statement.
144         * @see RunAfterTestClassCallbacks
145         */
146        private Statement withAfterTestClassCallbacks(Statement next, TestContextManager testContextManager) {
147                return new RunAfterTestClassCallbacks(next, testContextManager);
148        }
149
150        /**
151         * Wrap the supplied {@link Statement} with a {@code ProfileValueChecker} statement.
152         * @see ProfileValueChecker
153         */
154        private Statement withProfileValueCheck(Statement next, Class<?> testClass) {
155                return new ProfileValueChecker(next, testClass, null);
156        }
157
158        /**
159         * Wrap the supplied {@link Statement} with a {@code TestContextManagerCacheEvictor} statement.
160         * @see TestContextManagerCacheEvictor
161         */
162        private Statement withTestContextManagerCacheEviction(Statement next, Class<?> testClass) {
163                return new TestContextManagerCacheEvictor(next, testClass);
164        }
165
166        /**
167         * Get the {@link TestContextManager} associated with the supplied test class.
168         * @param testClass the test class to be managed; never {@code null}
169         */
170        static TestContextManager getTestContextManager(Class<?> testClass) {
171                Assert.notNull(testClass, "Test Class must not be null");
172                return testContextManagerCache.computeIfAbsent(testClass, TestContextManager::new);
173        }
174
175
176        private static class TestContextManagerCacheEvictor extends Statement {
177
178                private final Statement next;
179
180                private final Class<?> testClass;
181
182                TestContextManagerCacheEvictor(Statement next, Class<?> testClass) {
183                        this.next = next;
184                        this.testClass = testClass;
185                }
186
187                @Override
188                public void evaluate() throws Throwable {
189                        try {
190                                this.next.evaluate();
191                        }
192                        finally {
193                                testContextManagerCache.remove(this.testClass);
194                        }
195                }
196        }
197
198}