AndroidUiTestingUtils
AndroidUiTestingUtils copied to clipboard
A set of TestRules, ActivityScenarios and utils to facilitate UI and screenshot testing under given configurations: FontSizes, Locales...
Android UI testing utils
A set of TestRules, ActivityScenarios and utils to facilitate UI testing & screenshot testing under certain configurations, independent of the UI testing framework you are using. For screenshot testing, it supports Jetpack Compose, android Views (e.g. custom Views, ViewHolders, etc.) and Activities. Currently, with this library you can easily change the following configurations in your instrumented tests:
- Locale (also Pseudolocales en_XA & ar_XB)
- Font size
- Orientation
- Dark mode /Day-Night mode
- Display size
You can find out why verifying our design under such configurations is important in this blog post:
In the near future, there are plans to also support, among others:
- FragmentScenario
- Reduce snapshot testing flakiness
- Folding features
- Enable Accessibility features
Table of Contents
- Integration
- Usage
- Configuration
- Screnshot testing examples
- Activity
- Android View
- Jetpack Compose
- Fragment
- Utils
- Reading on screenshot testing
- Standard UI testing
- Code attributions
- Contributing
Integration
Add jitpack to your root build.gradle file:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
Add a dependency to build.gradle
dependencies {
androidTestImplementation 'com.github.sergio-sastre:AndroidUiTestingUtils:1.1.2'
}
Usage
Configuration
First, you need to add the following permission and activities to your debug/manifest
<!-- Required for ActivityScenarios -->
<application
...
<activity android:name="sergio.sastre.uitesting.utils.activityscenario.ActivityScenarioConfigurator$PortraitSnapshotConfiguredActivity"/>
<activity android:name="sergio.sastre.uitesting.utils.activityscenario.ActivityScenarioConfigurator$LandscapeSnapshotConfiguredActivity"
android:screenOrientation="landscape"/>
...
</application>
To enable pseudolocales en_XA & ar_XB for your screenshot tests, add this to your build.gradle.
android {
...
buildTypes {
...
debug {
pseudoLocalesEnabled true
}
}
}
Pre 1.1.2
You also need to add the following permission to your debug/manifest if not using version 1.1.2 or higher
<!-- Required to change the Locale via LocaleTestRule (required for snapshot testing Activities only) -->
<uses-permission
android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
...
Screenshot testing examples
The examples use pedrovgs/Shot. It'd also work with any other on-device screenshot testing framework, like Facebook screenshot-tests-for-android, Dropbox Dropshots or with a custom screenshot testing solution.
Activity
@get:Rule
val locale = LocaleTestRule("en")
@get:Rule
val fontSize = FontSizeTestRule(FontSize.HUGE).withTimeOut(inMillis = 15_000) // default is 10_000
@get:Rule
val displaySize = DisplaySizeTestRule(DisplaySize.LARGEST).withTimeOut(inMillis = 15_000)
@Test
fun snapActivityTest() {
// Locale, FontSize & DisplaySize are only supported via TestRules for Activities
val activity = ActivityScenarioConfigurator.ForActivity()
.setOrientation(Orientation.LANDSCAPE)
.setUiMode(UiMode.NIGHT)
.launch(YourActivity::class.java)
val activity = activityScenario.waitForActivity()
compareScreenshot(activity = activity, name = "your_unique_test_name")
activityScenario.close()
}
Android View
// example for ViewHolder
@Test
fun snapViewHolderTest() {
val activityScenario =
ActivityScenarioConfigurator.ForView()
.setFontSize(FontSize.NORMAL)
.setLocale("en")
.setInitialOrientation(Orientation.PORTRAIT)
.setUiMode(UiMode.DAY)
.setDisplaySize(DisplaySize.SMALL)
.launchConfiguredActivity()
val activity = activityScenario.waitForActivity()
val layout = waitForView {
// IMPORTANT: To inherit the configuration, inflate layout inside the activity with its context
activity.inflate(R.layout.your_view_holder_layout)
}
// wait asynchronously for layout inflation
val viewHolder = waitForView {
YourViewHolder(layout).apply {
// bind data to ViewHolder here
...
}
}
compareScreenshot(
holder = viewHolder,
heightInPx = layout.height,
name = "your_unique_test_name"
)
activityScenario.close()
}
Warning: If the View under test contains system Locale dependent code, like NumberFormat.getInstance(Locale.getDefault()), the Locale formatting you've set via ActivityScenarioConfigurator.ForView().setLocale("my_locale") will not work. That's because NumberFormat is using the Locale of the Android system, and not that of the Activity we've configured. Beware of using instrumenation.targetContext in your tests when using getString() for the very same reason: use Activity's context instead. To solve that issue, you can do one of the following:
- Use
NumberFormat.getInstance(anyViewInsideActivity.context.locales[0])in your production code. - Use
LocaleTestRule("my_locale")in your tests instead ofActivityScenarioConfigurator.ForView().setLocale("my_locale").
Jetpack Compose
// needs an EmptyComposeRule to be compatible with ActivityScenario
@get:Rule
val composeTestRule = createEmptyComposeRule()
@Test
fun snapComposableTest() {
val activityScenario = ActivityScenarioConfigurator.ForComposable()
.setFontSize(FontSize.SMALL)
.setLocale("de")
.setInitialOrientation(Orientation.PORTRAIT)
.setUiMode(UiMode.DAY)
.setDisplaySize(DisplaySize.LARGE)
.launchConfiguredActivity()
.onActivity {
it.setContent {
AppTheme { // this theme must use isSystemInDarkTheme() internally
yourComposable()
}
}
}
activityScenario.waitForActivity()
compareScreenshot(rule = composeTestRule, name = "your_unique_test_name")
activityScenario.close()
}
Warning: If the Composable under test contains system Locale dependent code, like NumberFormat.getInstance(Locale.getDefault()), the Locale formatting you've set via ActivityScenarioConfigurator.ForComposable().setLocale("my_locale") will not work. That's because NumberFormat is using the Locale of the Android system, and not that of the Activity we've configured, which is applied to the LocaleContext of our Composables. To solve that issue, you can do one of the following:
- Use
NumberFormat.getInstance(LocaleContext.current.locales[0])in your production code. - Use
LocaleTestRule("my_locale")in your tests instead ofActivityScenarioConfigurator.ForComposable().setLocale("my_locale").
Fragment
As of version 1.1.2, it is not supported, but will be added in the next releases. For now, you can circumvent it by creating a custom empty Activity containing the fragment under test, and do like in the example to snapshot test Activities. Keep in mind that you need to define an additional empty Activity for landscape mode to support landscape orientation.
Utils
-
waitForActivity: This method is analog to the one defined in pedrovgs/Shot. It's also available in this library for compatibility with other screenshot testing frameworks like Facebook screenshot-tests-for-android.
-
waitForView: Inflates the layout in the main thread and waits till the inflation happens, returning the inflated view. You will need to inflate layouts with the activity context created from the ActivityScenario of this library for the configurations to become effective.
-
activity.inflate(R.layout_of_your_view): Use it to inflate android Views with the activity's context configuration. In doing so, the configuration becomes effective in the view. It also adds the view to the Activity's root.
Reading on screenshot testing
- An introduction to snapshot testing on Android in 2021
- The secrets of effectively snapshot testing on Android 🔓
- UI tests vs. snapshot tests on Android: which one should I write? 🤔
- Design a pixel perfect Android app 🎨
Standard UI testing
For standard UI testing, you can use the same approach as for snapshot testing Activities. In case you do not want to use ActivityScenario at all in your tests, the following TestRules and methods are provided:
@get:Rule
val locale = LocaleTestRule("en")
@get:Rule
val fontSize = FontSizeTestRule(FontSize.HUGE).withTimeOut(inMillis = 15_000) // default is 10_000
@get:Rule
val displaySize = DisplaySizeTestRule(DisplaySize.LARGEST).withTimeOut(inMillis = 15_000)
@get:Rule
val uiMode = DayNightRule(UiMode.NIGHT)
activity.rotateTo(Orientation.LANDSCAPE)
WARNING: When using DisplaySizeTestRule and FontSizeTesRule together in the same test, make sure your emulator has enough RAM and VM heap to avoid Exceptions when running the tests. The recommended configuration is the following:
- RAM: 4GB
- VM heap: 1GB
Code attributions
This library has been possible due to the work others have done previously. Most TestRules are based on code written by others:
- LocaleTestRule -> Screengrab (pre 1.1.2 only)
- FontSizeTestRule -> Novoda/espresso-support
- UiModeTestRule -> AdevintaSpain/Barista
- Orientation change for activities -> Shopify/android-testify
Contributing
- Create an issue in this repo
- Fork the repo Road to effective snapshot testing
- In that repo, add an example and test where the bug is reproducible/ and showcasing the new feature.
- Once pushed, add a link to the PR in the issue created in this repo and add @sergio-sastre as a reviewer.
- Once reviewed and approved, create an issue in this repo.
- Fork this repo and add the approved code from the other repo to this one (no example or test needed). Add @sergio-sastre as a reviewer.
- Once approved, I will merge the code in both repos, and you will be added as a contributor to Android UI testing utils as well as Road to effective snapshot testing.
I'll try to make the process easier in the future if I see many issues/feature requests incoming :)
Android UI testing utils logo modified from one by Freepik - Flaticon