1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.platform.test.longevity; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.Mockito.argThat; 23 import static org.mockito.Mockito.atLeastOnce; 24 import static org.mockito.Mockito.doAnswer; 25 import static org.mockito.Mockito.doReturn; 26 import static org.mockito.Mockito.mock; 27 import static org.mockito.Mockito.never; 28 import static org.mockito.Mockito.spy; 29 import static org.mockito.Mockito.times; 30 import static org.mockito.Mockito.verify; 31 import static org.mockito.MockitoAnnotations.initMocks; 32 33 import android.os.Bundle; 34 import android.platform.test.rule.TestWatcher; 35 36 import org.junit.After; 37 import org.junit.AfterClass; 38 import org.junit.Assert; 39 import org.junit.Before; 40 import org.junit.BeforeClass; 41 import org.junit.ClassRule; 42 import org.junit.Rule; 43 import org.junit.Test; 44 import org.junit.runner.Description; 45 import org.junit.runner.RunWith; 46 import org.junit.runner.notification.Failure; 47 import org.junit.runner.notification.RunListener; 48 import org.junit.runner.notification.RunNotifier; 49 import org.junit.runners.JUnit4; 50 import org.junit.runners.model.FrameworkMethod; 51 import org.junit.runners.model.Statement; 52 import org.mockito.ArgumentCaptor; 53 import org.mockito.Mock; 54 import org.mockito.exceptions.base.MockitoAssertionError; 55 56 import java.util.ArrayList; 57 import java.util.List; 58 59 /** Unit tests for the {@link LongevityClassRunner}. */ 60 public class LongevityClassRunnerTest { 61 // Used to test things that are generated reflectively, e.g. dynamic rules. 62 private static final List<String> sLogs = new ArrayList<>(); 63 64 public static class LoggingRule extends TestWatcher { 65 private final String mName; 66 LoggingRule(String name)67 public LoggingRule(String name) { 68 mName = name; 69 } 70 71 @Override starting(Description description)72 public void starting(Description description) { 73 sLogs.add(String.format("%s starting", mName)); 74 } 75 76 @Override finished(Description description)77 public void finished(Description description) { 78 sLogs.add(String.format("%s finished", mName)); 79 } 80 } 81 82 public static class InjectedRule1 extends LoggingRule { InjectedRule1()83 public InjectedRule1() { 84 super("Injected rule 1"); 85 } 86 } 87 88 public static class InjectedRule2 extends LoggingRule { InjectedRule2()89 public InjectedRule2() { 90 super("Injected rule 2"); 91 } 92 } 93 94 // We use two @Test methods here to distinguish between testing for class 95 // rules vs. test rules. This is for testing only; we still advocate for 96 // one @Test method per class. 97 @RunWith(JUnit4.class) 98 public static class LoggingTestWithRules { 99 @ClassRule public static LoggingRule classRule = new LoggingRule("Hardcoded class rule"); 100 101 @Rule public LoggingRule testRule = new LoggingRule("Hardcoded test rule"); 102 103 @Test test1()104 public void test1() { 105 sLogs.add("Test 1 execution"); 106 } 107 108 @Test test2()109 public void test2() { 110 sLogs.add("Test 2 execution"); 111 } 112 } 113 114 // A sample test class to test the runner with. 115 @RunWith(JUnit4.class) 116 public static class NoOpTest { 117 @BeforeClass beforeClassMethod()118 public static void beforeClassMethod() {} 119 120 @Before beforeMethod()121 public void beforeMethod() {} 122 123 @Test testMethod()124 public void testMethod() {} 125 126 @After afterMethod()127 public void afterMethod() {} 128 129 @AfterClass afterClassMethod()130 public static void afterClassMethod() {} 131 } 132 133 @RunWith(JUnit4.class) 134 public static class FailingTest extends NoOpTest { 135 @Test testMethod()136 public void testMethod() { 137 throw new RuntimeException("I failed."); 138 } 139 } 140 141 @Mock private RunNotifier mRunNotifier; 142 143 private LongevityClassRunner mRunner; 144 145 private static final Statement PASSING_STATEMENT = 146 new Statement() { 147 public void evaluate() throws Throwable { 148 // No-op. 149 } 150 }; 151 152 private static final Statement FAILING_STATEMENT = 153 new Statement() { 154 public void evaluate() throws Throwable { 155 throw new RuntimeException("I failed."); 156 } 157 }; 158 159 // A failure message for assertion calls in Mockito stubs. These assertion failures will cause 160 // the runner under test to fail but will not trigger a test failure directly. This message is 161 // used to filter failures reported by the mocked RunNotifier and re-throw the ones injected in 162 // the spy. 163 private static final String ASSERTION_FAILURE_MESSAGE = "Test assertions failed"; 164 165 @Before setUp()166 public void setUp() { 167 initMocks(this); 168 sLogs.clear(); 169 } 170 171 /** 172 * Test that the {@link BeforeClass} methods are added to the test statement as {@link Before} 173 * methods. 174 */ 175 @Test testBeforeClassMethodsAddedAsBeforeMethods()176 public void testBeforeClassMethodsAddedAsBeforeMethods() throws Throwable { 177 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 178 // Spy the withBeforeClasses() method to check that the method does not make changes to the 179 // statement despite the presence of a @BeforeClass method. 180 doAnswer( 181 invocation -> { 182 Statement returnedStatement = (Statement) invocation.callRealMethod(); 183 // If this assertion fails, mRunNofitier will fire a test failure. 184 Assert.assertEquals( 185 ASSERTION_FAILURE_MESSAGE, 186 returnedStatement, 187 invocation.getArgument(0)); 188 return returnedStatement; 189 }) 190 .when(mRunner) 191 .withBeforeClasses(any(Statement.class)); 192 // Spy the addRunBefores() method to check that the @BeforeClass method is added to the 193 // @Before methods. 194 doAnswer( 195 invocation -> { 196 List<FrameworkMethod> methodList = 197 (List<FrameworkMethod>) invocation.getArgument(1); 198 // If any of these assertions fail, mRunNofitier will fire a test 199 // failure. 200 // There should be two methods. 201 Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, methodList.size(), 2); 202 // The first one should be the @BeforeClass one. 203 Assert.assertEquals( 204 ASSERTION_FAILURE_MESSAGE, 205 methodList.get(0).getName(), 206 "beforeClassMethod"); 207 // The second one should be the @Before one. 208 Assert.assertEquals( 209 ASSERTION_FAILURE_MESSAGE, 210 methodList.get(1).getName(), 211 "beforeMethod"); 212 return invocation.callRealMethod(); 213 }) 214 .when(mRunner) 215 .addRunBefores(any(Statement.class), any(List.class), any(Object.class)); 216 // Run the runner. 217 mRunner.run(mRunNotifier); 218 verifyForAssertionFailures(mRunNotifier); 219 // Verify that the stubbed methods are indeed called. 220 verify(mRunner, times(1)).withBeforeClasses(any(Statement.class)); 221 verify(mRunner, times(1)) 222 .addRunBefores(any(Statement.class), any(List.class), any(Object.class)); 223 } 224 225 /** 226 * Test that the {@link AfterClass} methods are added to the test statement as potential {@link 227 * After} methods. 228 */ 229 @Test testAfterClassMethodsAddedAsAfterMethods()230 public void testAfterClassMethodsAddedAsAfterMethods() throws Throwable { 231 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 232 // Spy the withAfterClasses() method to check that the method returns an instance of 233 // LongevityClassRunner.RunAfterClassMethodsOnTestFailure. 234 doAnswer( 235 invocation -> { 236 Statement returnedStatement = (Statement) invocation.callRealMethod(); 237 // If this assertion fails, mRunNofitier will fire a test failure. 238 Assert.assertTrue( 239 ASSERTION_FAILURE_MESSAGE, 240 returnedStatement 241 instanceof 242 LongevityClassRunner.RunAfterClassMethodsOnTestFailure); 243 return returnedStatement; 244 }) 245 .when(mRunner) 246 .withAfterClasses(any(Statement.class)); 247 // Spy the addRunAfters() method to check that the method returns an instance of 248 // LongevityClassRunner.RunAfterMethods. 249 doAnswer( 250 invocation -> { 251 Statement returnedStatement = (Statement) invocation.callRealMethod(); 252 // If any of these assertions fail, mRunNotifier will fire a test 253 // failure. 254 Assert.assertTrue( 255 ASSERTION_FAILURE_MESSAGE, 256 returnedStatement 257 instanceof LongevityClassRunner.RunAfterMethods); 258 // The second argument should only contain the @After method. 259 List<FrameworkMethod> afterMethodList = 260 (List<FrameworkMethod>) invocation.getArgument(1); 261 Assert.assertEquals( 262 ASSERTION_FAILURE_MESSAGE, afterMethodList.size(), 1); 263 Assert.assertEquals( 264 ASSERTION_FAILURE_MESSAGE, 265 afterMethodList.get(0).getName(), 266 "afterMethod"); 267 // The third argument should only contain the @AfterClass method. 268 List<FrameworkMethod> afterClassMethodList = 269 (List<FrameworkMethod>) invocation.getArgument(2); 270 Assert.assertEquals( 271 ASSERTION_FAILURE_MESSAGE, afterClassMethodList.size(), 1); 272 Assert.assertEquals( 273 ASSERTION_FAILURE_MESSAGE, 274 afterClassMethodList.get(0).getName(), 275 "afterClassMethod"); 276 return returnedStatement; 277 }) 278 .when(mRunner) 279 .addRunAfters( 280 any(Statement.class), any(List.class), any(List.class), any(Object.class)); 281 // Run the runner. 282 mRunner.run(mRunNotifier); 283 verifyForAssertionFailures(mRunNotifier); 284 // Verify that the stubbed methods are indeed called. 285 verify(mRunner, times(1)).withAfterClasses(any(Statement.class)); 286 verify(mRunner, times(1)) 287 .addRunAfters( 288 any(Statement.class), any(List.class), any(List.class), any(Object.class)); 289 } 290 291 /** 292 * Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as failed for a failed 293 * test. 294 */ 295 @Test testAfterClassMethodsHandling_marksFailure()296 public void testAfterClassMethodsHandling_marksFailure() throws Throwable { 297 // Initialization parameter does not matter as this test does not concern the tested class. 298 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 299 try { 300 mRunner.hasTestFailed(); 301 Assert.fail("Test status should not be able to be checked before it's run."); 302 } catch (Throwable e) { 303 Assert.assertTrue(e.getMessage().contains("should not be checked")); 304 } 305 // Create and partially run the runner with a failing statement. 306 Statement statement = 307 mRunner.withAfters( 308 null, 309 new NoOpTest(), 310 new Statement() { 311 public void evaluate() throws Throwable { 312 throw new RuntimeException("I failed."); 313 } 314 }); 315 try { 316 statement.evaluate(); 317 } catch (Throwable e) { 318 // Expected and no action needed. 319 } 320 Assert.assertTrue(mRunner.hasTestFailed()); 321 } 322 323 /** 324 * Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as passed for a passed 325 * test. 326 */ 327 @Test testRunAfterClassMethodsHandling_marksPassed()328 public void testRunAfterClassMethodsHandling_marksPassed() throws Throwable { 329 // Initialization parameter does not matter as this test does not concern the tested class. 330 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 331 // Checking test status before the statements are run should throw. 332 try { 333 mRunner.hasTestFailed(); 334 Assert.fail("Test status should not be able to be checked before it's run."); 335 } catch (Throwable e) { 336 Assert.assertTrue(e.getMessage().contains("should not be checked")); 337 } 338 // Create and partially run the runner with a passing statement. 339 Statement statement = 340 mRunner.withAfters( 341 null, 342 new NoOpTest(), 343 new Statement() { 344 public void evaluate() throws Throwable { 345 // Does nothing and thus passes. 346 } 347 }); 348 statement.evaluate(); 349 Assert.assertFalse(mRunner.hasTestFailed()); 350 } 351 352 /** Test that {@link AfterClass} methods are run as {@link After} methods for a passing test. */ 353 @Test testAfterClass_runAsAfterForPassingTest()354 public void testAfterClass_runAsAfterForPassingTest() throws Throwable { 355 // Initialization parameter does not matter as this test does not concern the tested class. 356 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 357 // For a passing test, the AfterClass method should be run in the statement returned from 358 // withAfters(). 359 Statement statement = 360 mRunner.withAfters( 361 null, // Passing null as parameter in the interface is not actually used. 362 new NoOpTest(), 363 PASSING_STATEMENT); 364 statement.evaluate(); 365 // Check that the @AfterClass method is called. 366 ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class); 367 verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any()); 368 Assert.assertEquals(methodsCaptor.getValue().size(), 1); 369 Assert.assertTrue( 370 ((FrameworkMethod) methodsCaptor.getValue().get(0)) 371 .getName() 372 .contains("afterClassMethod")); 373 } 374 375 /** 376 * Test that {@link AfterClass} methods are run as {@link AfterClass} method for a failing test. 377 */ 378 @Test testAfterClass_runAsAfterClassForFailingTest()379 public void testAfterClass_runAsAfterClassForFailingTest() throws Throwable { 380 // Initialization parameter does not matter as this test does not concern the tested class. 381 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 382 // For a failing test, the AfterClass method should be run in the statement returned from 383 // withAfterClass(). 384 doReturn(true).when(mRunner).hasTestFailed(); 385 Statement statement = mRunner.withAfterClasses(FAILING_STATEMENT); 386 try { 387 statement.evaluate(); 388 } catch (Throwable e) { 389 // Expected and no action needed. 390 } 391 // Check that the @AfterClass method is called. 392 ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class); 393 verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any()); 394 Assert.assertEquals(methodsCaptor.getValue().size(), 1); 395 Assert.assertTrue( 396 ((FrameworkMethod) methodsCaptor.getValue().get(0)) 397 .getName() 398 .contains("afterClassMethod")); 399 } 400 401 /** Test that {@link AfterClass} methods are only executed once for a passing test. */ 402 @Test testAfterClassRunOnlyOnce_passingTest()403 public void testAfterClassRunOnlyOnce_passingTest() throws Throwable { 404 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 405 mRunner.run(mRunNotifier); 406 verify(mRunner, times(1)) 407 .invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any()); 408 } 409 410 /** Test that {@link AfterClass} methods are only executed once for a failing test. */ 411 @Test testAfterClassRunOnlyOnce_failingTest()412 public void testAfterClassRunOnlyOnce_failingTest() throws Throwable { 413 mRunner = spy(new LongevityClassRunner(FailingTest.class)); 414 mRunner.run(mRunNotifier); 415 verify(mRunner, times(1)) 416 .invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any()); 417 } 418 419 /** Test that excluded classes are not executed. */ 420 @Test testIgnore_excludedClasses()421 public void testIgnore_excludedClasses() throws Throwable { 422 RunNotifier notifier = spy(new RunNotifier()); 423 RunListener listener = mock(RunListener.class); 424 notifier.addListener(listener); 425 Bundle ignores = new Bundle(); 426 ignores.putString(LongevityClassRunner.FILTER_OPTION, FailingTest.class.getCanonicalName()); 427 mRunner = spy(new LongevityClassRunner(FailingTest.class, ignores)); 428 mRunner.run(notifier); 429 verify(listener, times(1)).testIgnored(any()); 430 } 431 432 /** Test that the runner does not report iteration when iteration is not set. */ 433 @Test testReportIteration_noIterationSet()434 public void testReportIteration_noIterationSet() throws Throwable { 435 ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class); 436 RunNotifier notifier = mock(RunNotifier.class); 437 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 438 mRunner.run(notifier); 439 verify(notifier).fireTestStarted(captor.capture()); 440 Assert.assertFalse( 441 "Description class name should not contain the iteration number.", 442 captor.getValue() 443 .getClassName() 444 .matches( 445 String.join( 446 LongevityClassRunner.ITERATION_SEP_DEFAULT, 447 "^.*", 448 "[0-9]+$"))); 449 } 450 451 /** Test that the runner reports iteration when set and the default separator was used. */ 452 @Test testReportIteration_withIteration_withDefaultSeparator()453 public void testReportIteration_withIteration_withDefaultSeparator() throws Throwable { 454 ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class); 455 RunNotifier notifier = mock(RunNotifier.class); 456 mRunner = spy(new LongevityClassRunner(NoOpTest.class)); 457 mRunner.setIteration(7); 458 mRunner.run(notifier); 459 verify(notifier).fireTestStarted(captor.capture()); 460 Assert.assertTrue( 461 "Description class name should contain the iteration number.", 462 captor.getValue() 463 .getClassName() 464 .matches( 465 String.join( 466 LongevityClassRunner.ITERATION_SEP_DEFAULT, "^.*", "7$"))); 467 } 468 469 /** Test that the runner reports iteration when set and a custom separator was supplied. */ 470 @Test testReportIteration_withIteration_withCustomSeparator()471 public void testReportIteration_withIteration_withCustomSeparator() throws Throwable { 472 String sep = "--"; 473 Bundle args = new Bundle(); 474 args.putString(LongevityClassRunner.ITERATION_SEP_OPTION, sep); 475 476 ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class); 477 RunNotifier notifier = mock(RunNotifier.class); 478 mRunner = spy(new LongevityClassRunner(NoOpTest.class, args)); 479 mRunner.setIteration(7); 480 mRunner.run(notifier); 481 verify(notifier).fireTestStarted(captor.capture()); 482 Assert.assertTrue( 483 "Description class name should contain the iteration number.", 484 captor.getValue().getClassName().matches(String.join(sep, "^.*", "7$"))); 485 } 486 487 @Test testDynamicClassRules()488 public void testDynamicClassRules() throws Throwable { 489 Bundle args = new Bundle(); 490 args.putString( 491 LongevityClassRunner.DYNAMIC_OUTER_CLASS_RULES_OPTION, 492 InjectedRule1.class.getName()); 493 args.putString( 494 LongevityClassRunner.DYNAMIC_INNER_CLASS_RULES_OPTION, 495 InjectedRule2.class.getName()); 496 497 RunNotifier notifier = mock(RunNotifier.class); 498 mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args); 499 mRunner.run(notifier); 500 verifyForAssertionFailures(notifier); 501 assertThat(sLogs) 502 .containsExactly( 503 "Injected rule 1 starting", 504 "Hardcoded class rule starting", 505 "Injected rule 2 starting", 506 "Hardcoded test rule starting", 507 "Test 1 execution", 508 "Hardcoded test rule finished", 509 "Hardcoded test rule starting", 510 "Test 2 execution", 511 "Hardcoded test rule finished", 512 "Injected rule 2 finished", 513 "Hardcoded class rule finished", 514 "Injected rule 1 finished"); 515 } 516 517 @Test testDynamicTestRules()518 public void testDynamicTestRules() throws Throwable { 519 Bundle args = new Bundle(); 520 args.putString( 521 LongevityClassRunner.DYNAMIC_OUTER_TEST_RULES_OPTION, 522 InjectedRule1.class.getName()); 523 args.putString( 524 LongevityClassRunner.DYNAMIC_INNER_TEST_RULES_OPTION, 525 InjectedRule2.class.getName()); 526 527 RunNotifier notifier = mock(RunNotifier.class); 528 mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args); 529 mRunner.run(notifier); 530 verifyForAssertionFailures(notifier); 531 assertThat(sLogs) 532 .containsExactly( 533 "Hardcoded class rule starting", 534 "Injected rule 1 starting", 535 "Hardcoded test rule starting", 536 "Injected rule 2 starting", 537 "Test 1 execution", 538 "Injected rule 2 finished", 539 "Hardcoded test rule finished", 540 "Injected rule 1 finished", 541 "Injected rule 1 starting", 542 "Hardcoded test rule starting", 543 "Injected rule 2 starting", 544 "Test 2 execution", 545 "Injected rule 2 finished", 546 "Hardcoded test rule finished", 547 "Injected rule 1 finished", 548 "Hardcoded class rule finished"); 549 } 550 getMethodNameMatcher(String methodName)551 private List<FrameworkMethod> getMethodNameMatcher(String methodName) { 552 return argThat( 553 l -> 554 l.stream() 555 .anyMatch( 556 f -> ((FrameworkMethod) f).getName().contains(methodName))); 557 } 558 559 /** 560 * Verify that no test failure is fired because of an assertion failure in the stubbed methods. 561 * If the verfication fails, check whether it's due the injected assertions failing. If yes, 562 * throw that exception out; otherwise, throw the first exception. 563 */ verifyForAssertionFailures(final RunNotifier notifier)564 private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable { 565 try { 566 verify(notifier, never()).fireTestFailure(any()); 567 } catch (MockitoAssertionError e) { 568 ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class); 569 verify(notifier, atLeastOnce()).fireTestFailure(failureCaptor.capture()); 570 List<Failure> failures = failureCaptor.getAllValues(); 571 // Go through the failures, look for an known failure case from the above exceptions 572 // and throw the exception in the first one out if any. 573 for (Failure failure : failures) { 574 if (failure.getException().getMessage().contains(ASSERTION_FAILURE_MESSAGE)) { 575 throw failure.getException(); 576 } 577 } 578 // Otherwise, throw the exception from the first failure reported. 579 throw failures.get(0).getException(); 580 } 581 } 582 } 583