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}