Android provides a variety of tools and APIs that can help you create tests for different screen and window sizes.
DeviceConfigurationOverride
The DeviceConfigurationOverride
composable lets you override
configuration attributes to test multiple screen and window sizes in Compose
layouts. The ForcedSize
override fits any layout in the available space,
which lets you to run any
UI test on any screen size. For example, you can use a small phone form factor
to run all your UI tests, including UI tests for big phones, foldables, and
tablets.
DeviceConfigurationOverride(
DeviceConfigurationOverride.ForcedSize(DpSize(1280.dp, 800.dp))
) {
MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping.
}
Additionally, you can use this composable to set font scale, themes, and other properties that you might want to test on different window sizes.
Robolectric
Use Robolectric to run Compose or view-based UI tests on the JVM locally—no devices or emulators required. You can configure Robolectric to use specific screen sizes, among other useful properties.
In the following example from Now in Android, Robolectric is configured to emulate a screen size of 1000x1000 dp with a resolution of 480 dpi:
@RunWith(RobolectricTestRunner::class)
// Configure Robolectric to use a very large screen size that can fit all of the test sizes.
// This allows enough room to render the content under test without clipping or scaling.
@Config(qualifiers = "w1000dp-h1000dp-480dpi")
class NiaAppScreenSizesScreenshotTests { ... }
You can also set the qualifiers from the test body as done in this snippet from the Now in Android example:
val (width, height, dpi) = ...
// Set qualifiers from specs.
RuntimeEnvironment.setQualifiers("w${width}dp-h${height}dp-${dpi}dpi")
Note that RuntimeEnvironment.setQualifiers()
updates the system and
application resources with the new configuration but does not trigger any action
on active activities or other components.
You can read more in the Robolectric Device Configuration documentation.
Gradle-managed devices
The Gradle-managed devices (GMD) Android Gradle plugin lets you define the specifications of the emulators and real devices where your instrumented tests run. Create specifications for devices with different screen sizes to implement a testing strategy where certain tests must be run on certain screen sizes. By using GMD with Continuous Integration (CI), you can make sure that the appropriate tests run when required, provisioning and launching emulators and simplifying your CI setup.
android {
testOptions {
managedDevices {
devices {
// Run with ./gradlew nexusOneApi30DebugAndroidTest.
nexusOneApi30(com.android.build.api.dsl.ManagedVirtualDevice) {
device = "Nexus One"
apiLevel = 30
// Use the AOSP ATD image for better emulator performance
systemImageSource = "aosp-atd"
}
// Run with ./gradlew foldApi34DebugAndroidTest.
foldApi34(com.android.build.api.dsl.ManagedVirtualDevice) {
device = "Pixel Fold"
apiLevel = 34
systemImageSource = "aosp-atd"
}
}
}
}
}
You can find multiple examples of GMD in the testing-samples project.
Firebase Test Lab
Use Firebase Test Lab (FTL), or a similar device farm service, to run your tests on specific real devices that you might not have access to, such as foldables or tablets of varying sizes. Firebase Test Lab is a paid service with a free tier. FTL also supports running tests on emulators. These services improve the reliability and speed of instrumented testing because they can provision devices and emulators ahead of time.
For information about using FTL with GMD, see Scale your tests with Gradle-managed devices.
Test filtering with the test runner
An optimal test strategy shouldn't verify the same thing twice, so most of your UI tests don't need to run on multiple devices. Typically, you filter your UI tests by running all or most of them on a phone form factor and only a subset on devices with different screen sizes.
You can annotate certain tests to be run only with certain devices and then pass an argument to the AndroidJUnitRunner using the command that runs the tests.
For example, you can create different annotations:
annotation class TestExpandedWidth
annotation class TestCompactWidth
And use them on different tests:
class MyTestClass {
@Test
@TestExpandedWidth
fun myExample_worksOnTablet() {
...
}
@Test
@TestCompactWidth
fun myExample_worksOnPortraitPhone() {
...
}
}
You can then use the android.testInstrumentationRunnerArguments.annotation
property when running the tests to filter specific ones. For example, if you're
using Gradle-managed devices:
$ ./gradlew pixelTabletApi30DebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation='com.sample.TestExpandedWidth'
If you don't use GMD and you manage emulators on CI, first make sure that the correct emulator or device is ready and connected, and then pass the parameter to one of the Gradle commands to run instrumented tests:
$ ./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.annotation='com.sample.TestExpandedWidth'
Note that Espresso Device (see next section) can also filter tests by using device properties.
Espresso Device
Use Espresso Device to perform actions on emulators in tests using any type of instrumented tests, including Espresso, Compose, or UI Automator tests. These actions might include setting the screen size or toggling foldable states or postures. For example, you can control a foldable emulator and set it to tabletop mode. Espresso Device also contains JUnit rules and annotations to require certain features:
@RunWith(AndroidJUnit4::class)
class OnDeviceTest {
@get:Rule(order=1) val activityScenarioRule = activityScenarioRule<MainActivity>()
@get:Rule(order=2) val screenOrientationRule: ScreenOrientationRule =
ScreenOrientationRule(ScreenOrientation.PORTRAIT)
@Test
fun tabletopMode_playerIsDisplayed() {
// Set the device to tabletop mode.
onDevice().setTabletopMode()
onView(withId(R.id.player)).check(matches(isDisplayed()))
}
}
Note that Espresso Device is still in alpha stage and has the following requirements:
- Android Gradle Plugin 8.3 or higher
- Android Emulator 33.1.10 or higher
- Android virtual device that runs API level 24 or higher
Filter tests
Espresso Device can read the properties of connected devices to enable you to filter tests using annotations. If the annotated requirements are not met, the tests are skipped.
RequiresDeviceMode annotation
The RequiresDeviceMode
annotation can be used multiple times to indicate
a test that will run only if all the DeviceMode
values are supported
on the device.
class OnDeviceTest {
...
@Test
@RequiresDeviceMode(TABLETOP)
@RequiresDeviceMode(BOOK)
fun tabletopMode_playerIdDisplayed() {
// Set the device to tabletop mode.
onDevice().setTabletopMode()
onView(withId(R.id.player)).check(matches(isDisplayed()))
}
}
RequiresDisplay annotation
The RequiresDisplay
annotation lets you specify the width and height of
the device screen using size classes, which define dimension buckets
following the official window size classes.
class OnDeviceTest {
...
@Test
@RequiresDisplay(EXPANDED, COMPACT)
fun myScreen_expandedWidthCompactHeight() {
...
}
}
Resize displays
Use the setDisplaySize()
method to resize the dimensions of the screen
at runtime. Use the method in conjunction with the DisplaySizeRule
class, which makes sure that any changes made during tests are undone before the
next test.
@RunWith(AndroidJUnit4::class)
class ResizeDisplayTest {
@get:Rule(order = 1) val activityScenarioRule = activityScenarioRule<MainActivity>()
// Test rule for restoring device to its starting display size when a test case finishes.
@get:Rule(order = 2) val displaySizeRule: DisplaySizeRule = DisplaySizeRule()
@Test
fun resizeWindow_compact() {
onDevice().setDisplaySize(
widthSizeClass = WidthSizeClass.COMPACT,
heightSizeClass = HeightSizeClass.COMPACT
)
// Verify visual attributes or state restoration.
}
}
When you resize a display with setDisplaySize()
, you don't affect the density
of the device, so if a dimension doesn't fit in the target device, the test
fails with an UnsupportedDeviceOperationException
. To prevent tests from
being run in this case, use the RequiresDisplay
annotation to filter them out:
@RunWith(AndroidJUnit4::class)
class ResizeDisplayTest {
@get:Rule(order = 1) var activityScenarioRule = activityScenarioRule<MainActivity>()
// Test rule for restoring device to its starting display size when a test case finishes.
@get:Rule(order = 2) var displaySizeRule: DisplaySizeRule = DisplaySizeRule()
/**
* Setting the display size to EXPANDED would fail in small devices, so the [RequiresDisplay]
* annotation prevents this test from being run on devices outside the EXPANDED buckets.
*/
@RequiresDisplay(
widthSizeClass = WidthSizeClassEnum.EXPANDED,
heightSizeClass = HeightSizeClassEnum.EXPANDED
)
@Test
fun resizeWindow_expanded() {
onDevice().setDisplaySize(
widthSizeClass = WidthSizeClass.EXPANDED,
heightSizeClass = HeightSizeClass.EXPANDED
)
// Verify visual attributes or state restoration.
}
}
StateRestorationTester
The StateRestorationTester
class is used to test the state restoration
for composable components without recreating activities. This makes tests faster
and more reliable, as activity recreation is a complex process with multiple
synchronization mechanisms:
@Test
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
val stateRestorationTester = StateRestorationTester(composeTestRule)
// Set content through the StateRestorationTester object.
stateRestorationTester.setContent {
MyApp()
}
// Simulate a config change.
stateRestorationTester.emulateSavedInstanceStateRestore()
}
Window Testing library
The Window Testing library contains utilities to help you write tests that rely on or verify features related to window management, such as activity embedding or foldable features. The artifact is available through Google's Maven Repository.
For example, you can use the FoldingFeature()
function to generate a
custom FoldingFeature
, which you can use in Compose previews. In Java,
use the createFoldingFeature()
function.
In a Compose preview, you might implement FoldingFeature
in the following way:
@Preview(showBackground = true, widthDp = 480, heightDp = 480)
@Composable private fun FoldablePreview() =
MyApplicationTheme {
ExampleScreen(
displayFeatures = listOf(FoldingFeature(Rect(0, 240, 480, 240)))
)
}
Also, you can emulate display features in UI tests using the
TestWindowLayoutInfo()
function.
The following example simulates a FoldingFeature
with a
HALF_OPENED
vertical hinge in the screen's center, then checks whether the
layout is the one expected:
Compose
import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
import androidx.window.layout.FoldingFeature.State.Companion.HALF_OPENED
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
@RunWith(AndroidJUnit4::class)
class MediaControlsFoldingFeatureTest {
@get:Rule(order=1)
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
@get:Rule(order=2)
val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()
@Test
fun foldedWithHinge_foldableUiDisplayed() {
composeTestRule.setContent {
MediaPlayerScreen()
}
val hinge = FoldingFeature(
activity = composeTestRule.activity,
state = HALF_OPENED,
orientation = VERTICAL,
size = 2
)
val expected = TestWindowLayoutInfo(listOf(hinge))
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(expected)
composeTestRule.waitForIdle()
// Verify that the folding feature is detected and media controls shown.
composeTestRule.onNodeWithTag("MEDIA_CONTROLS").assertExists()
}
}
Views
import androidx.window.layout.FoldingFeature.Orientation
import androidx.window.layout.FoldingFeature.State
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
@RunWith(AndroidJUnit4::class)
class MediaControlsFoldingFeatureTest {
@get:Rule(order=1)
val activityRule = ActivityScenarioRule(MediaPlayerActivity::class.java)
@get:Rule(order=2)
val windowLayoutInfoPublisherRule = WindowLayoutInfoPublisherRule()
@Test
fun foldedWithHinge_foldableUiDisplayed() {
activityRule.scenario.onActivity { activity ->
val feature = FoldingFeature(
activity = activity,
state = State.HALF_OPENED,
orientation = Orientation.VERTICAL)
val expected = TestWindowLayoutInfo(listOf(feature))
windowLayoutInfoPublisherRule.overrideWindowLayoutInfo(expected)
}
// Verify that the folding feature is detected and media controls shown.
onView(withId(R.id.media_controls)).check(matches(isDisplayed()))
}
}
You can find more samples in the WindowManager project.
Additional resources
Documentation
Samples
- WindowManager sample
- Espresso Device samples
- Now In Android
- Uses screenshot testing to verify different screen sizes
Codelabs