diff --git a/docs/androidx_test.md b/docs/androidx_test.md index 4fc9ab339..bf1e52ed0 100644 --- a/docs/androidx_test.md +++ b/docs/androidx_test.md @@ -1,16 +1,18 @@ # AndroidX Test -Robolectric is intended to be fully compatible with Android's official testing libraries since [version 4.0](https://github.com/robolectric/robolectric/releases/tag/robolectric-4.0). -As such, we encourage you to try these new APIs and provide feedback. At some point, the Robolectric equivalents will -be deprecated and removed. Using the AndroidX Test APIs reduces the cognitive load for you as a developer, with just one -set of APIs to learn for the same Android concept, no matter if you are writing an Robolectric test or an instrumentation -test. Furthermore, it will make your tests more portable and compatible with our future plans. +Robolectric is intended to be fully compatible with Android's official testing libraries since +[version 4.0][robolectric-4.0-release]. As such, we encourage you to try these new APIs and provide +feedback. At some point, the Robolectric equivalents will be deprecated and removed. Using the +AndroidX Test APIs reduces the cognitive load for you as a developer, with just one set of APIs to +learn for the same Android concept, no matter if you are writing an Robolectric test or an +instrumentation test. Furthermore, it will make your tests more portable and compatible with our +future plans. ## TestRunner -It is now possible to use the AndroidX test runner in Robolectric tests. If you require a custom test runner, -please check out the [configuration and plugin API](javadoc/latest/org/robolectric/pluginapi/package-summary.html), -and let us know if there are any extension points missing that you require. +It is now possible to use the AndroidX test runner in Robolectric tests. If you require a custom +test runner, please check out the [configuration and plugin API][configuration-plugin-api], and let +us know if there are any extension points missing that you require. **Robolectric** @@ -56,8 +58,8 @@ and let us know if there are any extension points missing that you require. ## Application -Since most Android code is centric around a [`Context`](https://developer.android.com/reference/android/content/Context), -getting hold of your application’s context is a typical task for most tests. +Since most Android code is centric around a [`Context`][context-documentation], getting hold of your +application’s context is a typical task for most tests. **Robolectric** @@ -113,21 +115,21 @@ getting hold of your application’s context is a typical task for most tests. ## Activities -Robolectric provides [`Robolectric.setupActivity()`](javadoc/latest/org/robolectric/Robolectric.html#setupActivity(java.lang.Class)) -for the coarse-grained use case where you require a launched activity in the resumed state and visible for the user to interact with. +Robolectric provides [`Robolectric.setupActivity()`][robolectric-setup-activity] for the +coarse-grained use case where you require a launched activity in the resumed state and visible for +the user to interact with. -Robolectric also provides [`Robolectric.buildActivity()`](javadoc/latest/org/robolectric/Robolectric.html#buildActivity(java.lang.Class)), -which returns an [`ActivityController`](javadoc/latest/org/robolectric/android/controller/ActivityController.html) that allows -the developer to step through the [`Activity`](https://developer.android.com/reference/android/app/Activity) lifecycle. -This has proved problematic as it requires developers to fully understand valid lifecycle transitions and possible valid states. -Using an `Activity` in an invalid state has undefined behavior and can cause compatibility issues when running on different Android test runtimes -or when upgrading to newer versions of Robolectric. +Robolectric also provides [`Robolectric.buildActivity()`][robolectric-build-activity], which returns +an [`ActivityController`][activity-controller] that allows the developer to step through the +[`Activity`][activity-documentation] lifecycle. This has proved problematic as it requires +developers to fully understand valid lifecycle transitions and possible valid states. Using an +`Activity` in an invalid state has undefined behavior and can cause compatibility issues when +running on different Android test runtimes or when upgrading to newer versions of Robolectric. -[`ActivityScenario`](https://developer.android.com/reference/androidx/test/core/app/ActivityScenario) provides a -replacement for both of these use cases, but places tighter restrictions around lifecycle transitions, namely that -invalid or incomplete transitions are not possible. If you'd like a [`Rule`](https://junit.org/junit4/javadoc/latest/org/junit/Rule.html)-based -equivalent please use [`ActivityScenarioRule`](https://developer.android.com/reference/androidx/test/ext/junit/rules/ActivityScenarioRule) -instead. +[`ActivityScenario`][activity-scenario] provides a replacement for both of these use cases, but +places tighter restrictions around lifecycle transitions, namely that invalid or incomplete +transitions are not possible. If you'd like a [`Rule`][junit-rule]-based equivalent please use +[`ActivityScenarioRule`][activity-scenario-rule] instead. **Robolectric** @@ -218,20 +220,20 @@ instead. } ``` -Note that in Robolectric since both the test and UI event loop run on the same thread, synchronization is not an -issue. [`ActivityScenario.onActivity`](https://developer.android.com/reference/androidx/test/core/app/ActivityScenario#onActivity(androidx.test.core.app.ActivityScenario.ActivityAction%3CA%3E)) -provides a safe way of accessing the `Activity`, should you need to, that will be guaranteed to be compatible with our future plans. +Note that in Robolectric since both the test and UI event loop run on the same thread, +synchronization is not an issue. [`ActivityScenario.onActivity`][activity-scenario-on-activity] +provides a safe way of accessing the `Activity`, should you need to, that will be guaranteed to be +compatible with our future plans. ## Views -Robolectric has very limited APIs for [`View`](https://developer.android.com/reference/android/view/View) interaction. -In most cases, test writers can just use Android APIs, such as -[`Activity.findViewById()`](https://developer.android.com/reference/android/app/Activity#findViewById(int)) which was safe -since Robolectric tests do not have to worry about synchronization between test and UI threads. +Robolectric has very limited APIs for [`View`][view-documentation] interaction. In most cases, test +writers can just use Android APIs, such as [`Activity.findViewById()`][activity-find-view-by-id] +which was safe since Robolectric tests do not have to worry about synchronization between test and +UI threads. -[Espresso](https://developer.android.com/training/testing/espresso/) is the view -matching and interaction library of choice for instrumentation tests. Since Robolectric -4.0, Espresso APIs are now supported in Robolectric tests. +[Espresso][espresso] is the `View` matching and interaction library of choice for instrumentation +tests. Since Robolectric 4.0, Espresso APIs are now supported in Robolectric tests. === "Java" @@ -287,9 +289,8 @@ matching and interaction library of choice for instrumentation tests. Since Robo ## Fragments -AndroidX Test provides [`FragmentScenario`](https://developer.android.com/reference/androidx/fragment/app/testing/FragmentScenario), -which offers APIs to safely create your [`Fragment`](https://developer.android.com/reference/androidx/fragment/app/Fragment) -under test and drive it through valid transitions. +AndroidX Test provides [`FragmentScenario`][fragment-scenario], which offers APIs to safely create +your [`Fragment`][fragment-documentation] under test and drive it through valid transitions. === "Java" @@ -325,4 +326,22 @@ under test and drive it through valid transitions. } ``` -Read more about testing Fragments [here](https://developer.android.com/training/basics/fragments/testing). +Read more about testing Fragments [here][fragment-testing]. + +[activity-controller]: javadoc/latest/org/robolectric/android/controller/ActivityController.html +[activity-documentation]: https://developer.android.com/reference/android/app/Activity +[activity-find-view-by-id]: https://developer.android.com/reference/android/app/Activity#findViewById(int) +[activity-scenario]: https://developer.android.com/reference/androidx/test/core/app/ActivityScenario +[activity-scenario-on-activity]: https://developer.android.com/reference/androidx/test/core/app/ActivityScenario#onActivity(androidx.test.core.app.ActivityScenario.ActivityAction%3CA%3E) +[activity-scenario-rule]: https://developer.android.com/reference/androidx/test/ext/junit/rules/ActivityScenarioRule +[configuration-plugin-api]: https://github.com/robolectric/robolectric/releases/tag/robolectric-4.0 +[context-documentation]: https://developer.android.com/reference/android/content/Context +[espresso]: https://developer.android.com/training/testing/espresso +[fragment-documentation]: https://developer.android.com/reference/androidx/fragment/app/Fragment +[fragment-scenario]: https://developer.android.com/reference/androidx/fragment/app/testing/FragmentScenario +[fragment-testing]: https://developer.android.com/training/basics/fragments/testing +[junit-rule]: https://junit.org/junit4/javadoc/latest/org/junit/Rule.html +[robolectric-4.0-release]: https://github.com/robolectric/robolectric/releases/tag/robolectric-4.0 +[robolectric-build-activity]: javadoc/latest/org/robolectric/Robolectric.html#buildActivity +[robolectric-setup-activity]: javadoc/latest/org/robolectric/Robolectric.html#setupActivity(java.lang.Class) +[view-documentation]: https://developer.android.com/reference/android/view/View diff --git a/docs/architecture.md b/docs/architecture.md index c2f4ab51b..99fff8ef6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,33 +1,27 @@ # Robolectric architecture -Robolectric is a unit testing framework that allows Android code to be tested on -the JVM without the need for an emulator or device. This allows tests to run -very quickly in a more hermetic environment. Robolectric has a complex -architecture and makes use of many advanced features of the JVM, such as bytecode -instrumentation and custom [`ClassLoader`](https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html)s. -This document provides a high level overview of Robolectric's architecture. +Robolectric is a unit testing framework that allows Android code to be tested on the JVM without the +need for an emulator or device. This allows tests to run very quickly in a more hermetic +environment. Robolectric has a complex architecture and makes use of many advanced features of the +JVM, such as bytecode instrumentation and custom [`ClassLoader`][class-loader]s. This document +provides a high level overview of Robolectric's architecture. ## Android framework Jars and instrumentation -At the heart of Robolectric are the Android framework Jars and the bytecode -instrumentation. The Android framework Jars are a collection of Jar files that -are built directly from Android platform sources. There is a single Jar file for -each version of Android. These Jar files can be built by checking out an AOSP -repo and building the -[robolectric-host-android\_all](https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/Android.bp;l=112) -target. Unlike the `android.jar` (stubs jar) files managed by Android Studio, -which only contain public method signatures, the Robolectric android-all Jars -contain the implementation of the Android Java framework. This gives Robolectric -the ability to use as much real Android code as possible. A new android-all jar -is uploaded to MavenCentral for each Android release. You can see the current -android-all jars -[here](https://repo1.maven.org/maven2/org/robolectric/android-all/). - -However, the pristine android-all jars are not the ones used during tests. -Instead, Robolectric modifies the pristine android-all jars using bytecode -instrumentation (see -[`ClassInstrumentor`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java)). -It performs several modifications: +At the heart of Robolectric are the Android framework Jars and the bytecode instrumentation. The +Android framework Jars are a collection of Jar files that are built directly from Android platform +sources. There is a single Jar file for each version of Android. These Jar files can be built by +checking out an AOSP repo and building the +[robolectric-host-android\_all][robolectric-host-android-all] target. Unlike the `android.jar` +(stubs jar) files managed by Android Studio, which only contain public method signatures, the +Robolectric android-all Jars contain the implementation of the Android Java framework. This gives +Robolectric the ability to use as much real Android code as possible. A new android-all jar is +uploaded to MavenCentral for each Android release. You can see the current android-all +jars [here][android-all-jars]. + +However, the pristine android-all jars are not the ones used during tests. Instead, Robolectric +modifies the pristine android-all jars using bytecode instrumentation (see +[`ClassInstrumentor`][class-instrumentor]). It performs several modifications: 1. All Android methods, including constructors and static initializers, are modified to support `shadowing`. This allows any method call to the Android @@ -40,10 +34,9 @@ It performs several modifications: 1. Android constructors are specially modified to create shadow objects if a shadow class is bound to the Android class being instantiated. -1. Because the Android version of Java core classes (libcore) contains subtle - differences to the JDKs, certain problematic method calls have to be - intercepted and rewritten. See - [`AndroidInterceptors`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java). +1. Because the Android version of Java core classes (libcore) contains subtle differences to the + JDKs, certain problematic method calls have to be intercepted and rewritten. See + [`AndroidInterceptors`][android-interceptors]. 1. Native methods undergo special instrumentation. Currently, native methods are converted to no-op non-native methods that are shadowable by default. @@ -53,14 +46,12 @@ It performs several modifications: 1. The `final` keyword is stripped from classes and methods. 1. Some bespoke pieces of instrumentation, such as supporting - [`SparseArray.set()`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java#L204). + [`SparseArray.set()`][sparse-array-set]. -This instrumentation is typically performed when a new release of Robolectric is -made. These pre-instrumented Android-all jars are published on MavenCentral. See -the -[android-all-instrumented](https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/) -path. They are lazily downloaded and during tests runtime using -[`MavenArtifactFetcher`](https://github.com/robolectric/robolectric/blob/master/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java). +This instrumentation is typically performed when a new release of Robolectric is made. These +pre-instrumented Android-all jars are published on MavenCentral. See +the [android-all-instrumented][android-all-instrumented] path. They are lazily downloaded and during +tests runtime using [`MavenArtifactFetcher`][maven-artifact-fetcher]. Although Robolectric supports shadowing for Android framework classes, it is also possible for users to perform Robolectric instrumentation for any package @@ -68,24 +59,19 @@ also possible for users to perform Robolectric instrumentation for any package ## Shadows -By default, when an Android method is invoked during a Robolectric test, the real -Android framework code is invoked. This is because a lot of Android framework -classes are pure Java code (e.g., the -[`Intent`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/Intent.java) -class or the -[`org.json`](https://cs.android.com/android/platform/superproject/main/+/main:libcore/json/src/main/java/org/json/) -package) and that code can run on the JVM without any modifications needed. +By default, when an Android method is invoked during a Robolectric test, the real Android framework +code is invoked. This is because a lot of Android framework classes are pure Java code (e.g., the +[`Intent`][intent-source] class or the [`org.json`][org-json-package] package) and that code can run +on the JVM without any modifications needed. However, there are cases where Robolectric needs to intercept and replace Android method calls. This most commonly occurs when Android system service or native methods are invoked. To do this, Robolectric uses a system called Shadow classes. -Shadow classes are Java classes that contain the replacement code of Android -methods when they are invoked. Each shadow class is bound to specific Android -classes and methods through annotations. There are currently hundreds of shadow -classes that can be found -[here](https://github.com/robolectric/robolectric/tree/master/shadows/framework/src/main/java/org/robolectric/shadows). +Shadow classes are Java classes that contain the replacement code of Android methods when they are +invoked. Each shadow class is bound to specific Android classes and methods through annotations. +There are currently hundreds of shadow classes that can be found [here][shadows-list]. Shadow classes may optionally contain public APIs that can customize the behavior of the methods they are shadowing. @@ -95,21 +81,15 @@ defined implementation for Android classes. ## Shadow Packages and the Robolectric Annotation Processor -There are two categories of shadows: Robolectric’s built-in shadows that are -aggregated using the [Robolectric Annotation Processor -(RAP)](https://github.com/robolectric/robolectric/blob/master/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java), -and custom shadows that are commonly specified using `@Config(shadows = …)`. RAP -is configured to process all the shadow files that exist in Robolectric’s -code. The main shadow package is [framework -shadows](https://github.com/robolectric/robolectric/tree/master/shadows/framework), -which contain shadows for the Android framework. There are other shadow packages -in Robolectric's code, such as [httpclient -shadows](https://github.com/robolectric/robolectric/tree/master/shadows/httpclient), -but all of them outside of framework shadows are deprecated. When Robolectric is -built, each shadow package is processed by RAP and a -[ShadowProvider](https://github.com/robolectric/robolectric/blob/master/shadowapi/src/main/java/org/robolectric/internal/ShadowProvider.java) -file is generated. For example, to see the `ShadowProvider` for the framework -shadows, you can run: +There are two categories of shadows: Robolectric’s built-in shadows that are aggregated using the +[Robolectric Annotation Processor (RAP)][robolectric-annotation-processor], and custom shadows that +are commonly specified using `@Config(shadows = …)`. RAP is configured to process all the shadow +files that exist in Robolectric’s code. The main shadow package is +[framework shadows][framework-shadows], which contain shadows for the Android framework. There are +other shadow packages in Robolectric's code, such as [httpclient shadows][httpclient-shadows], but +all of them outside of framework shadows are deprecated. When Robolectric is built, each shadow +package is processed by RAP and a [ShadowProvider][shadow-provider] file is generated. For example, +to see the `ShadowProvider` for the framework shadows, you can run: ```shell ./gradlew :shadows:framework:assemble @@ -124,51 +104,41 @@ that implement `ShadowProvider` and the shadow classes contained in them. ## `Sandbox` and `ClassLoader` -Before a Robolectric test is executed, a -[`Sandbox`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java) -must be initialized. A `Sandbox` consists of some high-level structures that are -necessary to run a Robolectric test. It primarily contains a -[`SandboxClassLoader`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java), -which is a custom `ClassLoader` that is bound to a specific instrumented -Android-all jar. Sandboxes also contain the `ExecutorService` that serves as the -main thread (UI thread) as well as high-level instrumentation configuration. The -`SandboxClassLoader` is installed as the default `ClassLoader` for the test method. -When any Android class is requested, `SandboxClassLoader` will attempt to load the -Android class from the instrumented Android-all Jar first. The primary goal of -`SandboxClassLoader` is to ensure that classes from the `android.jar` stubs jar are -not inadvertently loaded. When classes from the `android.jar` stubs jar are -loaded, attempting to invoke any method on them will result in a -`RuntimeException("Stub!")` error. Typically, the Android stubs jar is on the -classpath during a Robolectric test, but it is important not to load classes -from the stubs jar. +Before a Robolectric test is executed, a [`Sandbox`][sandbox-source] must be initialized. A +`Sandbox` consists of some high-level structures that are necessary to run a Robolectric test. It +primarily contains a [`SandboxClassLoader`][sandbox-class-loader], which is a custom `ClassLoader` +that is bound to a specific instrumented Android-all jar. Sandboxes also contain the +`ExecutorService` that serves as the main thread (UI thread) as well as high-level instrumentation +configuration. The `SandboxClassLoader` is installed as the default `ClassLoader` for the test +method. When any Android class is requested, `SandboxClassLoader` will attempt to load the Android +class from the instrumented Android-all Jar first. The primary goal of `SandboxClassLoader` is to +ensure that classes from the `android.jar` stubs jar are not inadvertently loaded. When classes from +the `android.jar` stubs jar are loaded, attempting to invoke any method on them will result in a +`RuntimeException("Stub!")` error. Typically, the Android stubs jar is on the classpath during a +Robolectric test, but it is important not to load classes from the stubs jar. ## Invokedynamic Delegators and `ShadowWrangler` -This section provides more detail for `invokedynamic delegators` that were -referenced in the instrumentation section. For an overview of the -`invokedynamic` JVM instructions, you can search for articles or watch [YouTube -videos such as this](https://www.youtube.com/watch?v=KhiECfzyVt0). +This section provides more detail for `invokedynamic delegators` that were referenced in the +instrumentation section. For an overview of the `invokedynamic` JVM instructions, you can search for +articles or watch [YouTube videos such as this][invoke-dynamic-video]. To reiterate, for any Android method, Robolectric’s instrumentation adds an -`invokedynamic delegator` that is responsible for determining at runtime to -either invoke the real Android framework code or a shadow method. The first time -an Android method is invoked in a `Sandbox`, it will result in a call to one of -the bootstrap methods in -[`InvokeDynamicSupport`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java). -This will subsequently invoke the -[`ShadowWrangler.findShadowMethodHandle()`](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java#L170) -to determine if a shadow method exists for the method that is being invoked. If -a shadow method is available, a `MethodHandle` to it will be returned. Otherwise, a -`MethodHandle` for the original framework code will be returned. +`invokedynamic delegator` that is responsible for determining at runtime to either invoke the real +Android framework code or a shadow method. The first time an Android method is invoked in a +`Sandbox`, it will result in a call to one of the bootstrap methods in +[`InvokeDynamicSupport`][invoke-dynamic-support]. This will subsequently invoke the +[`ShadowWrangler.findShadowMethodHandle()`][shadow-wrangler-find-shadow-method-handle] to determine +if a shadow method exists for the method that is being invoked. If a shadow method is available, a +`MethodHandle` to it will be returned. Otherwise, a `MethodHandle` for the original framework code +will be returned. ## Test lifecycle -There is a lot of work done by Robolectric before and after a test is run. -Besides the `Sandbox` and `ClassLoader` initialization mentioned above, there is -also extensive Android environment initialization that occurs before each test. -The high-level class for this is -[`AndroidTestEnvironment`](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java). -This involves: +There is a lot of work done by Robolectric before and after a test is run. Besides the `Sandbox` and +`ClassLoader` initialization mentioned above, there is also extensive Android environment +initialization that occurs before each test. The high-level class for this is +[`AndroidTestEnvironment`][android-test-environment]. This involves: * Initializing up the `Looper` mode (i.e., the scheduler) * Initializing system and app resources @@ -180,31 +150,53 @@ This involves: * Creating app directories It is possible for users to extend the test environment setup using -[`TestEnvironmentLifecyclePlugin`](https://github.com/robolectric/robolectric/blob/master/pluginapi/src/main/java/org/robolectric/pluginapi/TestEnvironmentLifecyclePlugin.java). +[`TestEnvironmentLifecyclePlugin`][test-environment-lifecycle-plugin]. Similarly, after each test, many Android classes are reset during -[`RobolectricTestRunner.finallyAfterTest()`](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java#L299). -This will iterate over all shadows and invoke their static `@Resetter` methods. +[`RobolectricTestRunner.finallyAfterTest()`][robolectric-test-runner-finally-after-test]. This will +iterate over all shadows and invoke their static `@Resetter` methods. ## Plugin system -Many parts of Robolectric can be customized using a plugin system based on -Java’s -[`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). -This extensibility is useful when running Robolectric in more constrained -environments. For example, by default, most of the Robolectric classes are -designed to work in a Gradle/Android Studio environment. However, there are -companies (such as Google) that use alternate build systems (such as Bazel), and -it can be helpful to be able to customize the behavior of some core modules. - -The -[`pluginapi`](https://github.com/robolectric/robolectric/tree/master/pluginapi) -subproject contains many extension points of Robolectric. However, virtually any -class that is loaded by Robolectric’s -[`Injector`](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/Injector.java) -has the ability to use -[`PluginFinder`](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/PluginFinder.java), -which means it can be extended at runtime. - -Typically, `ServiceLoaders` plugins can be easily written using the -[`AutoService`](https://github.com/google/auto/tree/main/service) project. +Many parts of Robolectric can be customized using a plugin system based on Java’s +[`ServiceLoader`][service-loader]. This extensibility is useful when running Robolectric in more +constrained environments. For example, by default, most of the Robolectric classes are designed to +work in a Gradle/Android Studio environment. However, there are companies (such as Google) that use +alternate build systems (such as Bazel), and it can be helpful to be able to customize the behavior +of some core modules. + +The [`pluginapi`][plugin-api] subproject contains many extension points of Robolectric. However, +virtually any class that is loaded by Robolectric’s [`Injector`][injector-source] has the ability to +use [`PluginFinder`][plugin-finder], which means it can be extended at runtime. + +Typically, `ServiceLoaders` plugins can be easily written using the [`AutoService`][auto-service] +project. + +[android-all-jars]: https://repo1.maven.org/maven2/org/robolectric/android-all +[android-all-instrumented]: https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented +[android-interceptors]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java +[android-test-environment]: https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java +[auto-service]: https://github.com/google/auto/tree/main/service +[class-instrumentor]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java +[class-loader]: https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html +[framework-shadows]: https://github.com/robolectric/robolectric/tree/master/shadows/framework +[httpclient-shadows]: https://github.com/robolectric/robolectric/tree/master/shadows/httpclient +[injector-source]: https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/Injector.java +[intent-source]: https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/Intent.java +[invoke-dynamic-support]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java +[invoke-dynamic-video]: https://www.youtube.com/watch?v=KhiECfzyVt0 +[maven-artifact-fetcher]: https://github.com/robolectric/robolectric/blob/master/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java +[org-json-package]: https://cs.android.com/android/platform/superproject/main/+/main:libcore/json/src/main/java/org/json +[plugin-api]: https://github.com/robolectric/robolectric/tree/master/pluginapi +[plugin-finder]: https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/PluginFinder.java +[robolectric-annotation-processor]: https://github.com/robolectric/robolectric/blob/master/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java +[robolectric-host-android-all]: https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/Android.bp;l=112 +[robolectric-test-runner-finally-after-test]: https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java#L299 +[sandbox-class-loader]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java +[sandbox-source]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java +[service-loader]: https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html +[shadow-provider]: https://github.com/robolectric/robolectric/blob/master/shadowapi/src/main/java/org/robolectric/internal/ShadowProvider.java +[shadow-wrangler-find-shadow-method-handle]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java#L170 +[shadows-list]: https://github.com/robolectric/robolectric/tree/master/shadows/framework/src/main/java/org/robolectric/shadows +[sparse-array-set]: https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java#L204 +[test-environment-lifecycle-plugin]: https://github.com/robolectric/robolectric/blob/master/pluginapi/src/main/java/org/robolectric/pluginapi/TestEnvironmentLifecyclePlugin.java diff --git a/docs/automated-migration.md b/docs/automated-migration.md index 5ee65c792..ca7513c21 100644 --- a/docs/automated-migration.md +++ b/docs/automated-migration.md @@ -6,7 +6,8 @@ hide: # Automated Migration -Robolectric provides an automated migration tool to help keep your test suite up to date with Robolectric API changes. It's based on [Error Prone](https://errorprone.info/docs/patching)'s refactoring tools. +Robolectric provides an automated migration tool to help keep your test suite up to date with +Robolectric API changes. It's based on [Error Prone][error-prone-refactoring]'s refactoring tools. The migration tool will make changes directly to source files in your codebase, which you can review and commit to your source control system. @@ -14,7 +15,8 @@ The migration tool will make changes directly to source files in your codebase, 1. Make sure you're using a recent version of Gradle (4.10 or newer). -2. [Configure your project](https://errorprone.info/docs/installation) to integrate Error Prone. Quick config for Gradle (usually in your module's `build.gradle`/`build.gradle.kts` file): +2. [Configure your project][error-prone-setup] to integrate Error Prone. Quick config for Gradle ( + usually in your module's `build.gradle`/`build.gradle.kts` file): === "Groovy" @@ -79,3 +81,6 @@ The migration tool will make changes directly to source files in your codebase, 5. Update your project to the new version of Robolectric. The migration tool will make a best effort attempt to adjust the source code, but there might be more complicated situations that it cannot handle and that need to be converted manually. + +[error-prone-refactoring]: https://errorprone.info/docs/patching +[error-prone-setup]: https://errorprone.info/docs/installation diff --git a/docs/best-practices.md b/docs/best-practices.md index ecbb57b33..475cf9ed3 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -2,13 +2,32 @@ ## Best Practices -**DO** test layout inflation in your Robolectric test and ensure that click listeners are set up correctly by testing the [`Activity`](https://developer.android.com/reference/android/app/Activity) and layout interaction directly, rather than mocking the [`LayoutInflater`](https://developer.android.com/reference/android/view/LayoutInflater) or providing an abstraction over the [`View`](https://developer.android.com/reference/android/view/View). - -**DO** use public lifecycle APIs (e.g., via [`Robolectric.buildActivity()`](javadoc/latest/org/robolectric/Robolectric.html#buildActivity(java.lang.Class))), rather than exposing `@VisibleForTesting` methods when testing Android components such as `Activity`s and [`Service`](https://developer.android.com/reference/android/app/Service)s. Calling those methods directly makes it difficult to refactor the code under test later. - -**DO** limit the number of threads that are running during each test. Rogue threads often cause test pollution because they are not automatically cleaned up between tests. Oftentimes threads are inadvertently spawned when using third-party libraries (e.g., for networking) or background processing components. One of the main sources of additional threads during tests are [`ExecutorService`](https://developer.android.com/reference/kotlin/java/util/concurrent/ExecutorService)s that maintain thread pools. If possible, mock dependent components that spawn threads, or use [`MoreExecutors.directExecutor()`](https://guava.dev/releases/31.1-jre/api/docs/com/google/common/util/concurrent/MoreExecutors.html#directExecutor()). If it's necessary to run multiple threads during a test, make sure to explicitly stop all threads and `ExecutorService`s to avoid test pollution. - -**DON'T** mock or spy on Android classes that will be acted on by other Android code (e.g. [`Context`](https://developer.android.com/reference/android/content/Context), [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences), and many others). Stubbing is very brittle and can lead to breakages on Robolectric or Android Platform upgrades. The small exceptions to this rule are classes with very narrow responsibilities, such as event listeners. +**DO** test layout inflation in your Robolectric test and ensure that click listeners are set up +correctly by testing the [`Activity`][activity-documentation] and layout interaction directly, +rather than mocking the [`LayoutInflater`][layout-inflater] or providing an abstraction over the +[`View`][view-documentation]. + +**DO** use public lifecycle APIs (e.g., via +[`Robolectric.buildActivity()`][robolectric-build-activity]), rather than exposing +`@VisibleForTesting` methods when testing Android components such as `Activity`s and +[`Service`][service-documentation]s. Calling those methods directly makes it difficult to refactor +the code under test later. + +**DO** limit the number of threads that are running during each test. Rogue threads often cause test +pollution because they are not automatically cleaned up between tests. Oftentimes threads are +inadvertently spawned when using third-party libraries (e.g., for networking) or background +processing components. One of the main sources of additional threads during tests are +[`ExecutorService`][executor-service]s that maintain thread pools. If possible, mock dependent +components that spawn threads, or use +[`MoreExecutors.directExecutor()`][more-executors-direct-executor]. If it's necessary to run +multiple threads during a test, make sure to explicitly stop all threads and `ExecutorService`s to +avoid test pollution. + +**DON'T** mock or spy on Android classes that will be acted on by other Android code (e.g. +[`Context`][context-documentation], [`SharedPreferences`][shared-preferences], and many others). +Stubbing is very brittle and can lead to breakages on Robolectric or Android Platform upgrades. The +small exceptions to this rule are classes with very narrow responsibilities, such as event +listeners. ## Limitations @@ -26,20 +45,43 @@ Below is an incomplete list of differences you may encounter: #### Timezone handling -When using [`SimpleDateFormat#parse()`](https://developer.android.com/reference/kotlin/java/text/SimpleDateFormat#parse) to parse a timezone using the `Z` marker, Android supports time zones with a colon (i.e. `08:00`), while OpenJDK doesn't (i.e. `0800`). +When using [`SimpleDateFormat#parse()`][simple-date-format-parse] to parse a timezone using the `Z` +marker, Android supports time zones with a colon (i.e. `08:00`), while OpenJDK doesn't (i.e. +`0800`). To work around this: - If your min SDK version is 24 or higher, you can use the `X` marker instead. -- Use the `java.time` API, with [library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) enabled. +- Use the `java.time` API, with [library desugaring][library-desugaring] enabled. - Run the corresponding tests using regular Android tests. -**Related issues:** [#1030](https://github.com/robolectric/robolectric/issues/1030), [#1257](https://github.com/robolectric/robolectric/issues/1257), [#5220](https://github.com/robolectric/robolectric/issues/5220). +**Related issues:** [#1030][robolectric-issue-1030], [#1257][robolectric-issue-1257], +[#5220][robolectric-issue-5220]. #### Month names -When using [`DateTimeFormatter.ofPattern(String)`](https://developer.android.com/reference/kotlin/java/time/format/DateTimeFormatter#ofpattern) to create a formatter displaying a short month name (using the `MMM` pattern), Android will use three-characters names (i.e. `Jan`), while OpenJDK will use three-characters names followed by a period (i.e. `Jan.`). +When using [`DateTimeFormatter.ofPattern(String)`][date-time-formatter-of-pattern] to create a +formatter displaying a short month name (using the `MMM` pattern), Android will use three-characters +names (i.e. `Jan`), while OpenJDK will use three-characters names followed by a period (i.e. +`Jan.`). In this case, we recommend running the corresponding tests as regular Android tests. -**Related issues:** [#7910](https://github.com/robolectric/robolectric/issues/7910). +**Related issues:** [#7910][robolectric-issue-7910]. + +[activity-documentation]: https://developer.android.com/reference/android/app/Activity +[context-documentation]: https://developer.android.com/reference/android/content/Context +[date-time-formatter-of-pattern]: https://developer.android.com/reference/kotlin/java/time/format/DateTimeFormatter#ofpattern +[executor-service]: https://developer.android.com/reference/kotlin/java/util/concurrent/ExecutorService +[layout-inflater]: https://developer.android.com/reference/android/view/LayoutInflater +[library-desugaring]: https://developer.android.com/studio/write/java8-support#library-desugaring +[more-executors-direct-executor]: https://guava.dev/releases/33.3.1-jre/api/docs/com/google/common/util/concurrent/MoreExecutors.html#directExecutor() +[robolectric-build-activity]: javadoc/latest/org/robolectric/Robolectric.html#buildActivity(java.lang.Class) +[robolectric-issue-1030]: https://github.com/robolectric/robolectric/issues/1030 +[robolectric-issue-1257]: https://github.com/robolectric/robolectric/issues/1257 +[robolectric-issue-5220]: https://github.com/robolectric/robolectric/issues/5220 +[robolectric-issue-7910]: https://github.com/robolectric/robolectric/issues/7910 +[service-documentation]: https://developer.android.com/reference/android/app/Service +[shared-preferences]: https://developer.android.com/reference/android/content/SharedPreferences +[simple-date-format-parse]: https://developer.android.com/reference/kotlin/java/text/SimpleDateFormat#parse +[view-documentation]: https://developer.android.com/reference/android/view/View diff --git a/docs/build-system-integration.md b/docs/build-system-integration.md index 6ff238d9b..5958fb321 100644 --- a/docs/build-system-integration.md +++ b/docs/build-system-integration.md @@ -5,7 +5,10 @@ hide: # Build System Integration -Starting with [Robolectric 3.3](https://github.com/robolectric/robolectric/releases/tag/robolectric-3.3), the test runner will look for a file named `/com/android/tools/test_config.properties` on the classpath. If it is found, it will be used to provide the default manifest, resource, and asset locations for tests, without the need to specify `@Config(manifest = "...", resourceDir = "...", assetDir = "...")` in your tests. +Starting with [Robolectric 3.3][robolectric-3.3-release], the test runner will look for a file named +`/com/android/tools/test_config.properties` on the classpath. If it is found, it will be used to +provide the default manifest, resource, and asset locations for tests, without the need to specify +`@Config(manifest = "...", resourceDir = "...", assetDir = "...")` in your tests. This gives build system implementors the ability to perform manifest, asset and resource preprocessing and merging for tests using the same strategy it would when building the APK, rather than leaving it up to Robolectric. @@ -36,3 +39,5 @@ For binary resources support: ```properties android_resource_apk=/some/path/to/app/resources.ap_ ``` + +[robolectric-3.3-release]: https://github.com/robolectric/robolectric/releases/tag/robolectric-3.3 diff --git a/docs/building-robolectric.md b/docs/building-robolectric.md index 05c0f4854..ae6d072dd 100644 --- a/docs/building-robolectric.md +++ b/docs/building-robolectric.md @@ -4,27 +4,30 @@ This page describes how to set up a development environment to build and test Ro ## Installing Android SDK Tools -This can be achieved by either [installing Android Studio](https://developer.android.com/studio#download) (recommended), -or Android's [command line tools](https://developer.android.com/studio#command-line-tools-only). -We recommend using the latest stable release of Android Studio, because Robolectric uses the latest stable version of the -Android Gradle Plugin. +This can be achieved by either [installing Android Studio][android-studio-download] (recommended), +or Android's [command line tools][android-command-line-tools]. We recommend using the latest stable +release of Android Studio, because Robolectric uses the latest stable version of the Android Gradle +Plugin. -Robolectric's [integration tests](https://github.com/robolectric/robolectric/tree/master/integration_tests) -require Android Build Tools to be installed and specific SDK versions to be installed. Please check the relevant -modules to know which versions to install. +Robolectric's [integration tests][robolectric-integration-tests] require Android Build Tools to be +installed and specific SDK versions to be installed. Please check the relevant modules to know which +versions to install. ## Install Git and OpenJDK 17 JDK 17 is currently required to build Robolectric. Newer versions of the JDK (e.g. 21) will likely work, but may contain some rough edges. -1. [Install Git](https://git-scm.com/downloads) to download Robolectric source code. -2. Install OpenJDK 17 to build and test Robolectric. See [GitHub Action setup-java](https://github.com/actions/setup-java#supported-distributions) - to get the recommended OpenJDK distribution list. Any distribution that supports JDK 17 is recommended. Different operating - systems have different OpenJDK installation and configuration tutorials, please search the internet to learn how to do it. +1. [Install Git][git-downloads] to download Robolectric source code. +2. Install OpenJDK 17 to build and test Robolectric. See + [GitHub Action setup-java][github-actions-setup-java] to get the recommended OpenJDK distribution + list. Any distribution that supports JDK 17 is recommended. Different operating systems have + different OpenJDK installation and configuration tutorials, please search the internet to learn + how to do it. ## Download source code -Robolectric's source code is available on [GitHub](https://github.com/robolectric/robolectric). You can get by running the following command: +Robolectric's source code is available on [GitHub][robolectric-github]. You can get by running the +following command: ```shell git clone git@github.com:robolectric/robolectric.git @@ -63,10 +66,19 @@ emulator) by running: ./gradlew connectedAndroidTest ``` -If you're using Windows, it's recommended to use [PowerShell](https://github.com/PowerShell/PowerShell) in -[Windows Terminal](https://github.com/microsoft/terminal). +If you're using Windows, it's recommended to use [PowerShell][power-shell] in +[Windows Terminal][windows-terminal]. ## Next step Once you're up and running, you can have a look at the [architecture](architecture.md) documentation to learn more about Robolectric's components. + +[android-command-line-tools]: https://developer.android.com/studio#command-line-tools-only +[android-studio-download]: https://developer.android.com/studio#download +[git-downloads]: https://git-scm.com/downloads +[github-actions-setup-java]: https://github.com/actions/setup-java#supported-distributions +[power-shell]: https://github.com/PowerShell/PowerShell +[robolectric-github]: https://github.com/robolectric/robolectric +[robolectric-integration-tests]: https://github.com/robolectric/robolectric/tree/master/integration_tests +[windows-terminal]: https://github.com/microsoft/terminal