xref: /aosp_15_r20/external/robolectric/ARCHITECTURE.md (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
1# Robolectric architecture
2
3Robolectric is a unit testing framework that allows Android code to be tested on
4the JVM without the need for an emulator or device. This allows tests to run
5very quickly in a more hermetic environment. Robolectric has a complex
6architecture and makes use of many advanced features of the JVM such as bytecode
7instrumentation and custom ClassLoaders. This document provides a high level
8overview of the architecture of Robolectric.
9
10# Android framework Jars and instrumentation
11
12At the heart of Robolectric are the Android framework Jars and the bytecode
13instrumentation. The Android framework Jars are a collection of Jar files that
14are built directly from Android platform sources. There is a single Jar file for
15each version of Android. These Jar files can be built by checking out an AOSP
16repo and building the
17[robolectric-host-android\_all](https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/Android.bp;l=99)
18target. Unlike the android.jar (stubs jar) files managed by Android Studio,
19which only contain public method signatures, the Robolectric android-all Jars
20contain the implementation of the Android Java framework. This gives Robolectric
21the ability to use as much real Android code as possible. A new android-all jar
22is uploaded to MavenCentral for each Android release. You can see the current
23android-all jars
24[here](https://repo1.maven.org/maven2/org/robolectric/android-all/).
25
26However, the pristine android-all jars are not the ones used during tests.
27Instead, Robolectric modifies the pristine android-all jars using bytecode
28instrumentation (see
29[ClassInstrumentor](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java)).
30It performs several modifications:
31
321. All Android methods, including constructors and static initializers, are
33   modified to support `shadowing`. This allows any method call to the Android
34   framework to be intercepted by Robolectric and delegated to a shadow method.
35   At a high level, this is done by iterating over each Android method and
36   converting it into two methods: the original method (but renamed), and the
37   `invokedynamic delegator` which can optionally invoke shadow methods if they
38   are available.
39
401. Android constructors are specially modified to create shadow objects, if a
41   shadow class is bound to the Android class being instantiated.
42
431. Because the Android version of Java core classes (libcore) contain subtle
44   differences to the JDKs, certain problematic method calls have to be
45   intercepted and rewritten. See
46   [AndroidInterceptors](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/interceptors/AndroidInterceptors.java).
47
481. Native methods undergo special instrumentation. Currently native methods are
49   converted to no-op non-native methods that are shadowable by default.
50   However, there is now a native variant of each method also created. There is
51   more details about native code in a section below.
52
531. The `final` keyword is stripped from classes and methods.
54
551. Some bespoke pieces of instrumentation, such as supporting
56   [SparseArray.set](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java#L201).
57
58This instrumentation is typically performed when a new release of Robolectric is
59made. These pre-instrumented Android-all jars are published on MavenCentral. See
60the
61[android-all-instrumented](https://repo1.maven.org/maven2/org/robolectric/android-all-instrumented/)
62path. They are lazily downloaded and during tests runtime using
63[MavenArtifactFetcher](https://github.com/robolectric/robolectric/blob/master/plugins/maven-dependency-resolver/src/main/java/org/robolectric/internal/dependency/MavenArtifactFetcher.java).
64
65Although Robolectric supports shadowing for Android framework classes, it is
66also possible for users to perform Robolectric instrumentation for any package
67(with the exception of built in Java packages). This enables shadowing of
68arbitrary third-party code.
69
70# Shadows
71
72By default when an Android method is invoked during a Robolectric test, the real
73Android framework code is invoked. This is because a lot of Android framework
74classes are pure Java code (e.g the
75[Intent](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/Intent.java)
76class or the
77[org.json](https://cs.android.com/android/platform/superproject/main/+/main:libcore/json/src/main/java/org/json/)
78package) and that code can run on the JVM without any modifications needed.
79
80However, there are cases where Robolectric needs to intercept and replace
81Android method calls. This most commonly occurs when Android system service or
82native methods are invoked. To do this, Robolectric uses a system called Shadow
83classes.
84
85Shadow classes are Java classes that contain the replacement code of Android
86methods when they are invoked. Each shadow class is bound to specific Android
87classes and methods through annotations. There are currently hundreds of shadow
88classes that can be found
89[here](https://github.com/robolectric/robolectric/tree/master/shadows/framework/src/main/java/org/robolectric/shadows).
90
91Shadow classes may optionally contain public apis APIs that can customize the
92behavior of the methods they are shadowing.
93
94Robolectric allows tests to specify custom shadows as well to provide user
95defined implementation for Android classes.
96
97## Shadow Packages and the Robolectric Annotation Processor
98
99There are two categories of shadows: Robolectric’s built-in shadows that are
100aggregated using the [Robolectric Annotation Processor
101(RAP)](https://github.com/robolectric/robolectric/blob/master/processor/src/main/java/org/robolectric/annotation/processing/RobolectricProcessor.java),
102and custom shadows that are commonly specified using `@Config(shadows = …)`. RAP
103is configured to process all of the shadow files that exist in Robolectric’s
104code. The main shadow package is [framework
105shadows](https://github.com/robolectric/robolectric/tree/master/shadows/framework),
106which contain shadows for the Android framework. There are other shadow packages
107in Robolectric's code, such as [httpclient
108shadows](https://github.com/robolectric/robolectric/tree/master/shadows/httpclient),
109but all of them outside of framework shadows are deprecated. When Robolectric is
110built, each shadow package is processed by RAP and a
111[ShadowProvider](https://github.com/robolectric/robolectric/blob/master/shadowapi/src/main/java/org/robolectric/internal/ShadowProvider.java)
112file is generated. For example, to see the ShadowProvider for the framework
113shadows, you can run:
114
115```sh
116./gradlew :shadows:framework:assemble
117cat ./shadows/framework/build/generated/src/apt/main/org/robolectric/Shadows.java
118```
119
120In this file you will see the class `public class Shadows implements
121ShadowProvider`.
122
123During runtime, Robolectric will use ServiceLoader to detect all shadow packages
124that implement ShadowProvider and the shadow classes contained in them.
125
126# Sandbox and ClassLoader
127
128Before a Robolectric test is executed, a
129[Sandbox](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/Sandbox.java)
130must be initialized. A Sandbox consists of some high-level structures that are
131necessary to run a Robolectric test. It primarily contains a
132[SandboxClassLoader](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/SandboxClassLoader.java),
133which is a custom ClassLoader that is bound to a specific instrumented
134Android-all jar. Sandboxes also contain the ExecutorService that serves as the
135main thread (UI thread) as well as high-level instrumentation configuration. The
136SandboxClassLoader is installed as the default ClassLoader for the test method.
137When any Android class is requested, SandboxClassLoader will attempt to load the
138Android class from the instrumented Android-all Jar first. The primary goal of
139SandboxClassLoader is to ensure that classes from the android.jar stubs jar are
140not inadvertently loaded. When classes from the android.jar stubs jar are
141loaded, attempting to invoke any method on them will result in a
142`RuntimeException(“Stub!”)` error. Typically the Android stubs jar is on the
143class path during a Robolectric test, but it is important not to load classes
144from the stubs jar.
145
146# Invokedynamic Delegators and ShadowWrangler
147
148This section provides more detail for `invokedynamic delegators` that were
149referenced in the instrumentation section. For an overview of the
150`invokedynamic` JVM instructions, you can search for articles or watch [YouTube
151videos such as this](https://www.youtube.com/watch?v=KhiECfzyVt0).
152
153To reiterate, for any Android method, Robolectric’s instrumentation adds an
154`invokedynamic delegator` that is responsible for determining at runtime to
155either invoke the real Android framework code or a shadow method. The first time
156an Android method is invoked in a Sandbox, it will result in a call to one of
157the bootstrap methods in
158[InvokeDynamicSupport](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/InvokeDynamicSupport.java).
159This will subsequently invoke the
160[ShadowWrangler.findShadowMethodHandle](https://github.com/robolectric/robolectric/blob/master/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java#L197)
161to determine if a shadow method exists for the method that is being invoked.  If
162a shadow method is available a MethodHandle to it will be returned. Otherwise a
163MethodHandle for the original framework code will be returned.
164
165# Test lifecycle
166
167There is a lot of work done by Robolectric before and after a test is run.
168Besides the Sandbox and ClassLoader initialization mentioned above, there is
169also extensive Android environment initialization that occurs before each test.
170The high-level class for this is
171[AndroidTestEnvironment](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/android/internal/AndroidTestEnvironment.java).
172This involves:
173
174*   Initializing up the Looper mode (i.e. the scheduler)
175*   Initializing system and app resources
176*   Initializing the application context and system context
177*   Loading the Android manifest for the test
178*   Creating the Application object used for the test
179*   Initializing the [display configuration](https://robolectric.org/device-configuration/)
180*   Setting up the ActivityThread
181*   Creating app directories
182
183It is possible for users to extend the test environment setup using
184[TestEnvironmentLifecyclePlugin](https://github.com/robolectric/robolectric/blob/master/pluginapi/src/main/java/org/robolectric/pluginapi/TestEnvironmentLifecyclePlugin.java).
185
186Similarly, after each test, many Android classes are reset during
187[RobolectricTestRunner.finallyAfterTest](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/RobolectricTestRunner.java#L301).
188This will iterate over all shadows and invoke their static `@Resetter` methods.
189
190# Plugin System
191
192Many parts of Robolectric can be customized using a plugin system based on
193Java’s
194[ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html).
195This extensibility is useful when running Robolectric in more constrained
196environments. For example, by default, most of the Robolectric classes are
197designed to work in a Gradle/Android Studio environment. However, there are
198companies (such as Google) that use alternate build systems (such as Bazel), and
199it can be helpful to have the ability to customize the behavior of some core
200modules.
201
202The
203[pluginapi](https://github.com/robolectric/robolectric/tree/master/pluginapi/src)
204subproject contains many extension points of Robolectric. However, virtually any
205class that is loaded by Robolectric’s
206[Injector](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/Injector.java)
207has the ability to use
208[PluginFinder](https://github.com/robolectric/robolectric/blob/master/utils/src/main/java/org/robolectric/util/inject/PluginFinder.java),
209which means it can be extended at runtime.
210
211Typically ServiceLoaders plugins can be easily written using the
212[AutoService](https://github.com/google/auto/tree/main/service) project.
213
214