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