1*fa44fe6aSInna Palant# Test Harness for Jetpack Compose 2*fa44fe6aSInna Palant 3*fa44fe6aSInna Palant[](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[](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```