1 package org.robolectric; 2 3 import static com.google.common.truth.Truth.assertThat; 4 import static java.util.stream.Collectors.toSet; 5 import static org.junit.Assert.fail; 6 import static org.mockito.Mockito.mock; 7 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; 8 9 import android.annotation.SuppressLint; 10 import android.app.Application; 11 import android.os.Build; 12 import android.os.Handler; 13 import android.os.Looper; 14 import java.io.FileOutputStream; 15 import java.lang.reflect.Method; 16 import java.nio.charset.StandardCharsets; 17 import java.nio.file.Path; 18 import java.nio.file.Paths; 19 import java.nio.file.spi.FileSystemProvider; 20 import java.util.ArrayList; 21 import java.util.HashMap; 22 import java.util.List; 23 import java.util.Set; 24 import java.util.jar.JarEntry; 25 import java.util.jar.JarOutputStream; 26 import javax.annotation.Nonnull; 27 import javax.inject.Inject; 28 import javax.inject.Named; 29 import org.junit.After; 30 import org.junit.AfterClass; 31 import org.junit.AssumptionViolatedException; 32 import org.junit.Before; 33 import org.junit.BeforeClass; 34 import org.junit.FixMethodOrder; 35 import org.junit.Ignore; 36 import org.junit.Rule; 37 import org.junit.Test; 38 import org.junit.runner.Description; 39 import org.junit.runner.Result; 40 import org.junit.runner.RunWith; 41 import org.junit.runner.notification.Failure; 42 import org.junit.runner.notification.RunListener; 43 import org.junit.runner.notification.RunNotifier; 44 import org.junit.runners.JUnit4; 45 import org.junit.runners.MethodSorters; 46 import org.robolectric.RobolectricTestRunner.RobolectricFrameworkMethod; 47 import org.robolectric.android.internal.AndroidTestEnvironment; 48 import org.robolectric.annotation.Config; 49 import org.robolectric.annotation.experimental.LazyApplication; 50 import org.robolectric.annotation.experimental.LazyApplication.LazyLoad; 51 import org.robolectric.config.ConfigurationRegistry; 52 import org.robolectric.internal.AndroidSandbox.TestEnvironmentSpec; 53 import org.robolectric.internal.ShadowProvider; 54 import org.robolectric.junit.rules.SetSystemPropertyRule; 55 import org.robolectric.manifest.AndroidManifest; 56 import org.robolectric.pluginapi.Sdk; 57 import org.robolectric.pluginapi.TestEnvironmentLifecyclePlugin; 58 import org.robolectric.pluginapi.config.ConfigurationStrategy.Configuration; 59 import org.robolectric.pluginapi.perf.Metric; 60 import org.robolectric.pluginapi.perf.PerfStatsReporter; 61 import org.robolectric.plugins.DefaultSdkPicker; 62 import org.robolectric.plugins.SdkCollection; 63 import org.robolectric.util.TempDirectory; 64 import org.robolectric.util.TestUtil; 65 66 @SuppressWarnings("NewApi") 67 @RunWith(JUnit4.class) 68 public class RobolectricTestRunnerTest { 69 70 private RunNotifier notifier; 71 private List<String> events; 72 private String priorEnabledSdks; 73 private String priorAlwaysInclude; 74 private SdkCollection sdkCollection; 75 76 @Rule public SetSystemPropertyRule setSystemPropertyRule = new SetSystemPropertyRule(); 77 78 @Before setUp()79 public void setUp() throws Exception { 80 notifier = new RunNotifier(); 81 events = new ArrayList<>(); 82 notifier.addListener(new MyRunListener()); 83 84 priorEnabledSdks = System.getProperty("robolectric.enabledSdks"); 85 System.clearProperty("robolectric.enabledSdks"); 86 87 priorAlwaysInclude = System.getProperty("robolectric.alwaysIncludeVariantMarkersInTestName"); 88 System.clearProperty("robolectric.alwaysIncludeVariantMarkersInTestName"); 89 90 sdkCollection = TestUtil.getSdkCollection(); 91 } 92 93 @After tearDown()94 public void tearDown() throws Exception { 95 TestUtil.resetSystemProperty( 96 "robolectric.alwaysIncludeVariantMarkersInTestName", priorAlwaysInclude); 97 TestUtil.resetSystemProperty("robolectric.enabledSdks", priorEnabledSdks); 98 } 99 100 @Test ignoredTestCanSpecifyUnsupportedSdkWithoutExploding()101 public void ignoredTestCanSpecifyUnsupportedSdkWithoutExploding() throws Exception { 102 RobolectricTestRunner runner = 103 new RobolectricTestRunner( 104 TestWithOldSdk.class, 105 org.robolectric.RobolectricTestRunner.defaultInjector() 106 .bind(org.robolectric.pluginapi.SdkPicker.class, AllEnabledSdkPicker.class) 107 .build()); 108 runner.run(notifier); 109 assertThat(events) 110 .containsExactly( 111 "started: oldSdkMethod", 112 "failure: API level 11 is not available", 113 "finished: oldSdkMethod", 114 "ignored: ignoredOldSdkMethod"); 115 } 116 117 @Test failureInResetterDoesntBreakAllTests()118 public void failureInResetterDoesntBreakAllTests() throws Exception { 119 RobolectricTestRunner runner = 120 new SingleSdkRobolectricTestRunner( 121 TestWithTwoMethods.class, 122 SingleSdkRobolectricTestRunner.defaultInjector() 123 .bind( 124 TestEnvironmentSpec.class, 125 new TestEnvironmentSpec(AndroidTestEnvironmentWithFailingSetUp.class)) 126 .build()); 127 runner.run(notifier); 128 assertThat(events) 129 .containsExactly( 130 "started: first", 131 "failure: fake error in setUpApplicationState", 132 "finished: first", 133 "started: second", 134 "failure: fake error in setUpApplicationState", 135 "finished: second") 136 .inOrder(); 137 } 138 139 @Test noClassDefError_isReplacedByBetterLinkageError()140 public void noClassDefError_isReplacedByBetterLinkageError() throws Exception { 141 RobolectricTestRunner runner = 142 new SingleSdkRobolectricTestRunner( 143 TestWithTwoMethods.class, 144 SingleSdkRobolectricTestRunner.defaultInjector() 145 .bind( 146 TestEnvironmentSpec.class, 147 new TestEnvironmentSpec(AndroidTestEnvironmentThrowsLinkageError.class)) 148 .build()); 149 runner.run(notifier); 150 assertThat(events) 151 .containsExactly( 152 "started: first", 153 "failure: java.lang.ExceptionInInitializerError", 154 "finished: first", 155 "started: second", 156 "failure: java.lang.ExceptionInInitializerError", 157 "finished: second") 158 .inOrder(); 159 } 160 161 @Test failureInAppOnCreateDoesntBreakAllTests()162 public void failureInAppOnCreateDoesntBreakAllTests() throws Exception { 163 RobolectricTestRunner runner = 164 new SingleSdkRobolectricTestRunner(TestWithBrokenAppCreate.class); 165 runner.run(notifier); 166 assertThat(events) 167 .containsExactly( 168 "started: first", 169 "failure: fake error in application.onCreate", 170 "finished: first", 171 "started: second", 172 "failure: fake error in application.onCreate", 173 "finished: second") 174 .inOrder(); 175 } 176 177 @Test failureInAppOnTerminateDoesntBreakAllTests()178 public void failureInAppOnTerminateDoesntBreakAllTests() throws Exception { 179 RobolectricTestRunner runner = 180 new SingleSdkRobolectricTestRunner(TestWithBrokenAppTerminate.class); 181 runner.run(notifier); 182 assertThat(events) 183 .containsExactly( 184 "started: first", 185 "failure: fake error in application.onTerminate", 186 "finished: first", 187 "started: second", 188 "failure: fake error in application.onTerminate", 189 "finished: second") 190 .inOrder(); 191 } 192 193 @Test equalityOfRobolectricFrameworkMethod()194 public void equalityOfRobolectricFrameworkMethod() throws Exception { 195 Method method = TestWithTwoMethods.class.getMethod("first"); 196 RobolectricFrameworkMethod rfm16 = 197 new RobolectricFrameworkMethod( 198 method, 199 mock(AndroidManifest.class), 200 sdkCollection.getSdk(16), 201 mock(Configuration.class), 202 false); 203 RobolectricFrameworkMethod rfm17 = 204 new RobolectricFrameworkMethod( 205 method, 206 mock(AndroidManifest.class), 207 sdkCollection.getSdk(17), 208 mock(Configuration.class), 209 false); 210 RobolectricFrameworkMethod rfm16b = 211 new RobolectricFrameworkMethod( 212 method, 213 mock(AndroidManifest.class), 214 sdkCollection.getSdk(16), 215 mock(Configuration.class), 216 false); 217 218 assertThat(rfm16).isNotEqualTo(rfm17); 219 assertThat(rfm16).isEqualTo(rfm16b); 220 221 assertThat(rfm16.hashCode()).isEqualTo(rfm16b.hashCode()); 222 } 223 224 @Test shouldReportPerfStats()225 public void shouldReportPerfStats() throws Exception { 226 List<Metric> metrics = new ArrayList<>(); 227 PerfStatsReporter reporter = (metadata, metrics1) -> metrics.addAll(metrics1); 228 229 RobolectricTestRunner runner = 230 new SingleSdkRobolectricTestRunner( 231 TestWithTwoMethods.class, 232 SingleSdkRobolectricTestRunner.defaultInjector() 233 .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter}) 234 .build()); 235 236 runner.run(notifier); 237 238 Set<String> metricNames = metrics.stream().map(Metric::getName).collect(toSet()); 239 assertThat(metricNames).contains("initialization"); 240 } 241 242 @Test failedTest_shouldStillReportPerfStats()243 public void failedTest_shouldStillReportPerfStats() throws Exception { 244 List<Metric> metrics = new ArrayList<>(); 245 PerfStatsReporter reporter = (metadata, metrics1) -> metrics.addAll(metrics1); 246 247 RobolectricTestRunner runner = 248 new SingleSdkRobolectricTestRunner( 249 TestThatFails.class, 250 SingleSdkRobolectricTestRunner.defaultInjector() 251 .bind(PerfStatsReporter[].class, new PerfStatsReporter[] {reporter}) 252 .build()); 253 254 runner.run(notifier); 255 256 Set<String> metricNames = metrics.stream().map(Metric::getName).collect(toSet()); 257 assertThat(metricNames).contains("initialization"); 258 } 259 260 @Test shouldResetThreadInterrupted()261 public void shouldResetThreadInterrupted() throws Exception { 262 RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner(TestWithInterrupt.class); 263 runner.run(notifier); 264 assertThat(events) 265 .containsExactly( 266 "started: first", 267 "finished: first", 268 "started: second", 269 "failure: failed for the right reason", 270 "finished: second"); 271 } 272 273 @Test shouldDiagnoseUnexecutedRunnables()274 public void shouldDiagnoseUnexecutedRunnables() throws Exception { 275 RobolectricTestRunner runner = 276 new SingleSdkRobolectricTestRunner(TestWithUnexecutedRunnables.class); 277 runner.run(notifier); 278 assertThat(events) 279 .containsExactly( 280 "started: failWithNoRunnables", 281 "failure: failing with no runnables", 282 "finished: failWithNoRunnables", 283 "started: failWithUnexecutedRunnables", 284 "failure: failing with unexecuted runnable\n" 285 + "Suppressed: Main looper has queued unexecuted runnables. " 286 + "This might be the cause of the test failure. " 287 + "You might need a shadowOf(Looper.getMainLooper()).idle() call.", 288 "finished: failWithUnexecutedRunnables", 289 "started: assumptionViolationWithNoRunnables", 290 "ignored: assumptionViolationWithNoRunnables: assumption violated", 291 "finished: assumptionViolationWithNoRunnables", 292 "started: assumptionViolationWithUnexecutedRunnables", 293 "ignored: assumptionViolationWithUnexecutedRunnables: assumption violated", 294 "finished: assumptionViolationWithUnexecutedRunnables"); 295 } 296 297 ///////////////////////////// 298 299 /** To simulate failures. */ 300 public static class AndroidTestEnvironmentWithFailingSetUp extends AndroidTestEnvironment { 301 AndroidTestEnvironmentWithFailingSetUp( @amed"runtimeSdk") Sdk runtimeSdk, @Named("compileSdk") Sdk compileSdk, ShadowProvider[] shadowProviders, TestEnvironmentLifecyclePlugin[] lifecyclePlugins)302 public AndroidTestEnvironmentWithFailingSetUp( 303 @Named("runtimeSdk") Sdk runtimeSdk, 304 @Named("compileSdk") Sdk compileSdk, 305 ShadowProvider[] shadowProviders, 306 TestEnvironmentLifecyclePlugin[] lifecyclePlugins) { 307 super(runtimeSdk, compileSdk, shadowProviders, lifecyclePlugins); 308 } 309 310 @Override setUpApplicationState( String tmpDirName, Configuration configuration, AndroidManifest appManifest)311 public void setUpApplicationState( 312 String tmpDirName, Configuration configuration, AndroidManifest appManifest) { 313 // ConfigurationRegistry.instance is required for resetters. 314 ConfigurationRegistry.instance = new ConfigurationRegistry(configuration.map()); 315 throw new RuntimeException("fake error in setUpApplicationState"); 316 } 317 } 318 319 public static class AndroidTestEnvironmentThrowsLinkageError extends AndroidTestEnvironment { 320 321 public static final class UnloadableClass { 322 static { 323 if (true) { 324 throw new RuntimeException("error in static initializer"); 325 } 326 } 327 doStuff()328 public static void doStuff() {} 329 UnloadableClass()330 private UnloadableClass() {} 331 } 332 AndroidTestEnvironmentThrowsLinkageError( @amed"runtimeSdk") Sdk runtimeSdk, @Named("compileSdk") Sdk compileSdk, ShadowProvider[] shadowProviders, TestEnvironmentLifecyclePlugin[] lifecyclePlugins)333 public AndroidTestEnvironmentThrowsLinkageError( 334 @Named("runtimeSdk") Sdk runtimeSdk, 335 @Named("compileSdk") Sdk compileSdk, 336 ShadowProvider[] shadowProviders, 337 TestEnvironmentLifecyclePlugin[] lifecyclePlugins) { 338 super(runtimeSdk, compileSdk, shadowProviders, lifecyclePlugins); 339 } 340 341 @Override setUpApplicationState( String tmpDirName, Configuration configuration, AndroidManifest appManifest)342 public void setUpApplicationState( 343 String tmpDirName, Configuration configuration, AndroidManifest appManifest) { 344 UnloadableClass.doStuff(); 345 } 346 347 @Override resetState()348 public void resetState() {} 349 } 350 351 @Ignore 352 public static class TestWithOldSdk { 353 @Config(sdk = Build.VERSION_CODES.HONEYCOMB) 354 @Test oldSdkMethod()355 public void oldSdkMethod() throws Exception { 356 fail("I should not be run!"); 357 } 358 359 @Ignore("This test shouldn't run, and shouldn't cause the test runner to fail") 360 @Config(sdk = Build.VERSION_CODES.HONEYCOMB) 361 @Test ignoredOldSdkMethod()362 public void ignoredOldSdkMethod() throws Exception { 363 fail("I should not be run!"); 364 } 365 } 366 367 @Ignore 368 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 369 @Config(qualifiers = "w123dp-h456dp-land-hdpi") 370 public static class TestWithTwoMethods { 371 @Test first()372 public void first() throws Exception {} 373 374 @Test second()375 public void second() throws Exception {} 376 } 377 378 @Ignore 379 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 380 public static class TestThatFails { 381 @Test first()382 public void first() throws Exception { 383 throw new AssertionError(); 384 } 385 } 386 387 @Ignore 388 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 389 @Config(application = TestWithBrokenAppCreate.MyTestApplication.class) 390 @LazyApplication(LazyLoad.OFF) 391 public static class TestWithBrokenAppCreate { 392 @Test first()393 public void first() throws Exception {} 394 395 @Test second()396 public void second() throws Exception {} 397 398 public static class MyTestApplication extends Application { 399 @SuppressLint("MissingSuperCall") 400 @Override onCreate()401 public void onCreate() { 402 throw new RuntimeException("fake error in application.onCreate"); 403 } 404 } 405 } 406 407 @Ignore 408 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 409 @Config(application = TestWithBrokenAppTerminate.MyTestApplication.class) 410 @LazyApplication(LazyLoad.OFF) 411 public static class TestWithBrokenAppTerminate { 412 @Test first()413 public void first() throws Exception {} 414 415 @Test second()416 public void second() throws Exception {} 417 418 public static class MyTestApplication extends Application { 419 @SuppressLint("MissingSuperCall") 420 @Override onTerminate()421 public void onTerminate() { 422 throw new RuntimeException("fake error in application.onTerminate"); 423 } 424 } 425 } 426 427 @Ignore 428 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 429 public static class TestWithInterrupt { 430 @Test first()431 public void first() throws Exception { 432 Thread.currentThread().interrupt(); 433 } 434 435 @Test second()436 public void second() throws Exception { 437 TempDirectory tempDirectory = new TempDirectory("test"); 438 439 try { 440 Path jarPath = tempDirectory.create("some-jar").resolve("some.jar"); 441 try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jarPath.toFile()))) { 442 out.putNextEntry(new JarEntry("README.txt")); 443 out.write("hi!".getBytes(StandardCharsets.UTF_8)); 444 } 445 446 FileSystemProvider jarFSP = 447 FileSystemProvider.installedProviders().stream() 448 .filter(p -> p.getScheme().equals("jar")) 449 .findFirst() 450 .get(); 451 Path fakeJarFile = Paths.get(jarPath.toUri()); 452 453 // if Thread.interrupted() was true, this would fail in AbstractInterruptibleChannel: 454 jarFSP.newFileSystem(fakeJarFile, new HashMap<>()); 455 } finally { 456 tempDirectory.destroy(); 457 } 458 459 fail("failed for the right reason"); 460 } 461 } 462 463 /** Fixture for #shouldDiagnoseUnexecutedRunnables() */ 464 @Ignore 465 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 466 public static class TestWithUnexecutedRunnables { 467 468 @Test failWithUnexecutedRunnables()469 public void failWithUnexecutedRunnables() { 470 shadowMainLooper().pause(); 471 new Handler(Looper.getMainLooper()).post(() -> {}); 472 fail("failing with unexecuted runnable"); 473 } 474 475 @Test failWithNoRunnables()476 public void failWithNoRunnables() { 477 fail("failing with no runnables"); 478 } 479 480 @Test assumptionViolationWithUnexecutedRunnables()481 public void assumptionViolationWithUnexecutedRunnables() { 482 shadowMainLooper().pause(); 483 new Handler(Looper.getMainLooper()).post(() -> {}); 484 throw new AssumptionViolatedException("assumption violated"); 485 } 486 487 @Test assumptionViolationWithNoRunnables()488 public void assumptionViolationWithNoRunnables() { 489 throw new AssumptionViolatedException("assumption violated"); 490 } 491 } 492 493 /** Ignore the value of --Drobolectric.enabledSdks */ 494 public static class AllEnabledSdkPicker extends DefaultSdkPicker { 495 @Inject AllEnabledSdkPicker(@onnull SdkCollection sdkCollection)496 public AllEnabledSdkPicker(@Nonnull SdkCollection sdkCollection) { 497 super(sdkCollection, (String) null); 498 } 499 } 500 501 private class MyRunListener extends RunListener { 502 503 @Override testRunStarted(Description description)504 public void testRunStarted(Description description) { 505 events.add("run started: " + description.getMethodName()); 506 } 507 508 @Override testRunFinished(Result result)509 public void testRunFinished(Result result) { 510 events.add("run finished: " + result); 511 } 512 513 @Override testStarted(Description description)514 public void testStarted(Description description) { 515 events.add("started: " + description.getMethodName()); 516 } 517 518 @Override testFinished(Description description)519 public void testFinished(Description description) { 520 events.add("finished: " + description.getMethodName()); 521 } 522 523 @Override testAssumptionFailure(Failure failure)524 public void testAssumptionFailure(Failure failure) { 525 events.add( 526 "ignored: " + failure.getDescription().getMethodName() + ": " + failure.getMessage()); 527 } 528 529 @Override testIgnored(Description description)530 public void testIgnored(Description description) { 531 events.add("ignored: " + description.getMethodName()); 532 } 533 534 @Override testFailure(Failure failure)535 public void testFailure(Failure failure) { 536 Throwable exception = failure.getException(); 537 String message = exception.getMessage(); 538 if (message == null) { 539 message = exception.toString(); 540 } 541 for (Throwable suppressed : exception.getSuppressed()) { 542 message += "\nSuppressed: " + suppressed.getMessage(); 543 } 544 events.add("failure: " + message); 545 } 546 } 547 548 @Test shouldReportExceptionsInBeforeClass()549 public void shouldReportExceptionsInBeforeClass() throws Exception { 550 RobolectricTestRunner runner = 551 new SingleSdkRobolectricTestRunner(TestWithBeforeClassThatThrowsRuntimeException.class); 552 runner.run(notifier); 553 if (Boolean.getBoolean("robolectric.useLegacySandboxFlow")) { 554 assertThat(events.get(1)).startsWith("failure: fail"); 555 } else { 556 assertThat(events.get(0)).isEqualTo("failure: fail"); 557 } 558 } 559 560 @Ignore 561 public static class TestWithBeforeClassThatThrowsRuntimeException { 562 @BeforeClass beforeClass()563 public static void beforeClass() { 564 throw new RuntimeException("fail"); 565 } 566 567 @Test test()568 public void test() {} 569 } 570 571 @Test shouldInvokeAfterClass()572 public void shouldInvokeAfterClass() throws Exception { 573 RobolectricTestRunner runner = 574 new SingleSdkRobolectricTestRunner(TestClassWithAfterClass.class); 575 setSystemPropertyRule.set("RobolectricTestRunnerTest.wasAfterClassCalled", "false"); 576 runner.run(notifier); 577 assertThat(System.getProperty("RobolectricTestRunnerTest.wasAfterClassCalled")) 578 .isEqualTo("true"); 579 } 580 581 @Ignore 582 public static class TestClassWithAfterClass { 583 @AfterClass afterClass()584 public static void afterClass() { 585 System.setProperty("RobolectricTestRunnerTest.wasAfterClassCalled", "true"); 586 } 587 588 @Test test()589 public void test() {} 590 } 591 592 @Ignore 593 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 594 public static class TestWithIgnore { 595 @Test test()596 public void test() {} 597 598 // to verify @Ignore behavior 599 @Ignore 600 @Test ignoredTest()601 public void ignoredTest() {} 602 } 603 604 @Test shouldNotifyIgnoredTests()605 public void shouldNotifyIgnoredTests() throws Exception { 606 RobolectricTestRunner runner = new SingleSdkRobolectricTestRunner(TestWithIgnore.class); 607 runner.run(notifier); 608 assertThat(events) 609 .containsExactly("ignored: ignoredTest", "started: test", "finished: test") 610 .inOrder(); 611 } 612 } 613