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 * @ClassRule 055 * public static final SpringClassRule springClassRule = new SpringClassRule(); 056 * 057 * @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}