xref: /aosp_15_r20/external/accompanist/docs/testharness.md (revision fa44fe6ae8e729aa3cfe5c03eedbbf98fb44e2c6)
1# Test Harness for Jetpack Compose
2
3[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist)
4
5A library providing a test harness for UI components.
6
7## Background
8
9Device configuration (locale, font size, screen size, folding features, etc.) are device-wide
10properties, which makes it hard to automate tests that wants to vary these properties.
11One current solution is to run tests across a range of emulators or devices with different
12properties, and potentially filter tests to only run when specific conditions are met.
13This has the downside of increasing the number of devices to manage, higher complexity of
14configuring those devices, and more complicated test suites.
15
16With a Compose-only app, it is less common that the “physical” constraints of the device are
17directly used.
18Instead, state hoisting encourages isolating such constraints, and providing them to components via
19state that is observable via snapshots.
20The mechanism to do so is primarily via a set of composition locals, such as `LocalConfiguration`,
21`LocalDensity`, and others.
22The composition local mechanism provides a layer of indirection that permits overriding these
23constraints via those composition local hooks.
24
25## Test Harness
26
27`TestHarness` is an `@Composable` function, which takes a single slot of `@Composable` content.
28This content is the `@Composable` UI under test, so standard usage would look like the following:
29
30```kotlin
31@Test
32fun example() {
33    composeTestRule.setContent {
34        TestHarness {
35            MyComponent()
36        }
37    }
38
39    // assertions
40}
41```
42
43When no parameters of `TestHarness` are specified, `TestHarness` has no direct effect, and it would
44be equivalent to calling `MyComponent` directly.
45
46Specifying parameters of `TestHarness` results in overriding the default configuration for the
47content under-test, and will affect `MyComponent`.
48
49For example, specifying the `fontScale` parameter will change the effective font scale within
50the `TestHarness`:
51
52```kotlin
53@Test
54fun example() {
55    composeTestRule.setContent {
56        TestHarness(fontScale = 1.5f) {
57            Text("Configuration: ${LocalConfiguration.current.fontScale}")
58            Text("Density: ${LocalDensity.current.fontScale}")
59        }
60    }
61
62    composeTestRule.onNodeWithText("Configuration: 1.5").assertExists()
63    composeTestRule.onNodeWithText("Density: 1.5").assertExists()
64}
65```
66
67This allows testing UI for different font scales in a isolated way, without having to directly
68configure the device to use a different font scale.
69
70`TestHarness` also takes a `size: DpSize` parameter, to test a Composable at a particular size.
71
72```kotlin
73@Test
74fun example() {
75    composeTestRule.setContent {
76        TestHarness(size = DpSize(800.dp, 1000.dp)) {
77            MyComponent() // will be rendered at 800dp by 1000dp, even if the window is smaller
78        }
79    }
80}
81```
82
83See the full list of parameters and effects below.
84
85## Parameters
86
87The full list of parameters and their effects:
88
89| Parameter                           | Default value                                                                           | Effect                                                                                                             |
90|-------------------------------------|-----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
91| `size: DpSize`                      | `DpSize.Unspecified`                                                                    | If specified, overrides `LocalDensity` if needed to give the `DpSize` amount of space to the composable under test |
92| `darkMode: Boolean`                 | `isSystemInDarkTheme()`                                                                 | Overrides `LocalConfiguration.current.uiMode`                                                                      |
93| `fontScale: Float`                  | `LocalDensity.current.fontScale`                                                        | Overrides `LocalDensity.current.fontScale` and `LocalConfiguration.current.fontScale`                              |
94| `fontWeightAdjustment: Int?`        | `LocalConfiguration.current.fontWeightAdjustment` on API 31 and above, otherwise `null` | Overrides `LocalConfiguration.current.fontWeightAdjustment` on API 31 and above and not-null                       |
95| `locales: LocaleListCompat`         | `ConfigurationCompat.getLocales(LocalConfiguration.current)`                            | Overrides `LocalConfiguration.current.locales`                                                                     |
96| `layoutDirection: LayoutDirection?` | `null` (which uses the resulting locale layout direction)                               | Overrides `LocalLayoutDirection.current` and `LocalConfiguration.current.screenLayout`                             |
97
98## Implementation
99
100`TestHarness` works by overriding a set of composition locals provided to the content under test.
101
102The full list of composition locals that may be overridden by various parameters are:
103
104- `LocalConfiguration`
105- `LocalContext`
106- `LocalLayoutDirection`
107- `LocalDensity`
108- `LocalFontFamilyResolver`
109
110Any composable that depends on these composition locals should be testable via the test harness,
111because they will pull the overridden configuration information from them.
112This includes configuration-specific resources, because these are pulled from `LocalContext`.
113
114Testing a composable at a smaller size than the real screen space available is straightforward, but
115testing a composable at a larger size than the real screen space available is not. This is because
116the library and the testing APIs are sensitive to whether or not a composable is actually rendered
117within the window of the application.
118
119As a solution, `TestHarness` will override the `LocalDensity` to shrink the content as necessary
120for all of the specified `size: DpSize` to be displayed at once in the window space that is
121available. This results in the composable under test believing it has the specified space to work
122with, even if that is larger than the window of the application.
123
124## Limitations
125
126The test harness is simulating alternate configurations and sizes, so it does not exactly represent
127what a user would see on a real device.
128For that reason, the platform edges where Composables interact with the system more is where the
129test harness may break down and have issues.
130An incomplete list includes: dialogs (due to different `Window` instances), insets, soft keyboard
131interactions, and interop with `View`s.
132The density overriding when specifying a specific size to test a composable at also means that UI
133might be rendered in atypical ways, especially at the extreme of rendering a very large desktop-size
134UI on a small portrait phone.
135The mechanism that the test harness uses is also not suitable for production code: in production,
136the default configuration as specified by the user and the system should be used.
137
138The mechanism that the test harness uses to override the configuration (`ContextThemeWrapper`) is
139not fully supported by layoutlib. In particular, alternate resources are available just by using
140`TestHarness`.
141
142## Download
143
144[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-testharness)](https://search.maven.org/search?q=g:com.google.accompanist)
145
146```groovy
147repositories {
148    mavenCentral()
149}
150
151dependencies {
152    implementation "com.google.accompanist:accompanist-testharness:<version>"
153}
154```