Robolectric usage
AndroidUiTesting includes some special ActivityScenarioConfigurators
and FragmentScenarioConfigurators
that are additionally safe-thread, what allows to run unit tests in parallel without unexpected behaviours.
Check out some examples below. It uses Roborazzi as screenshot testing library.
Activity
Here is with Junit4 test rule
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapActivityTest {
@get:Rule
val robolectricScreenshotRule =
robolectricActivityScenarioForActivityRule(
config = ActivityConfigItem(
systemLocale = "en",
uiMode = UiMode.NIGHT,
theme = R.style.Custom_Theme,
orientation = Orientation.PORTRAIT,
fontSize = FontSize.NORMAL,
displaySize = DisplaySize.NORMAL,
),
deviceScreen = DeviceScreen.Phone.PIXEL_4A,
)
@Config(sdk = [30]) // Do not use qualifiers if using `DeviceScreen` in the Rule
@Test
fun snapActivity() {
robolectricScreenshotRule
.rootView
.captureRoboImage("path/MyActivity.png")
}
}
or without Junit4 test rules
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapActivityTest {
@Config(sdk = [30]) // Do not use qualifiers if using `setDeviceScreen()
@Test
fun snapActivity() {
val activityScenario =
RobolectricActivityScenarioConfigurator.ForActivity()
.setDeviceScreen(DeviceScreen.Phone.PIXEL_4A)
.setSystemLocale("en")
.setUiMode(UiMode.NIGHT)
.setOrientation(Orientation.PORTRAIT)
.setFontSize(FontSize.NORMAL)
.setDisplaySize(DisplaySize.NORMAL)
.launch(MyActivity::class.java)
activityScenario
.rootView
.captureRoboImage("path/MyActivity.png")
activityScenario.close()
}
}
Fragment
Here is with Junit4 test rule
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapFragmentTest {
@get:Rule
val robolectricScreenshotRule =
robolectricFragmentScenarioConfiguratorRule<MyFragment>(
fragmentArgs = bundleOf("arg_key" to "arg_value"),
config = FragmentConfigItem(
locale = "en_XA",
uiMode = UiMode.NIGHT,
theme = R.style.Custom_Theme,
orientation = Orientation.PORTRAIT,
fontSize = FontSize.NORMAL,
displaySize = DisplaySize.NORMAL,
),
deviceScreen = DeviceScreen.Phone.PIXEL_4A,
)
@Config(sdk = [30]) // Do not use qualifiers if using `DeviceScreen` in the Rule
@Test
fun snapFragment() {
robolectricScreenshotRule
.fragment
.requireView()
.captureRoboImage("path/MyFragment.png")
}
}
or without Junit4 test rules
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapFragmentTest {
@Config(sdk = [30]) // Do not use qualifiers if using `setDeviceScreen()
@Test
fun snapFragment() {
val fragmentScenario =
RobolectricFragmentScenarioConfigurator.ForFragment()
.setDeviceScreen(DeviceScreen.Phone.PIXEL_4A)
.setLocale("en_XA")
.setUiMode(UiMode.NIGHT)
.setTheme(R.style.Custom_Theme)
.setOrientation(Orientation.PORTRAIT)
.setFontSize(FontSize.NORMAL)
.setDisplaySize(DisplaySize.NORMAL)
.launchInContainer(
fragmentClass = MyFragment::class.java,
fragmentArgs = bundleOf("arg_key" to "arg_value"),
)
fragmentScenario
.waitForFragment()
.requireView()
.captureRoboImage("path/MyActivity.png")
fragmentScenario.close()
}
}
Android View
Here is with Junit4 test rule
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapViewHolderTest {
@get:Rule
val robolectricScreenshotRule =
RobolectricActivityScenarioForViewRule(
config = ViewConfigItem(
locale = "en_XA",
uiMode = UiMode.NIGHT,
theme = R.style.Custom_Theme,
orientation = Orientation.PORTRAIT,
fontSize = FontSize.NORMAL,
displaySize = DisplaySize.NORMAL,
),
deviceScreen = DeviceScreen.Phone.PIXEL_4A,
backgroundColor = TRANSPARENT,
)
@Config(sdk = [30]) // Do not use qualifiers if using `DeviceScreen` in the Rule
@Test
fun snapViewHolder() {
val activity = robolectricScreenshotRule.activity
val layout =
robolectricScreenshotRule.inflateAndWaitForIdle(R.layout.memorise_row)
val viewHolder = waitForMeasuredViewHolder {
MemoriseViewHolder(
container = layout,
itemEventListener = null,
animationDelay = 0L,
).apply {
bind(generateMemoriseItem(rightAligned = false, activity = activity))
}
}
viewHolder
.itemView
.captureRoboImage("path/MemoriseViewHolder.png")
}
}
or without Junit4 test rules
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapViewHolderTest {
@Config(sdk = [30]) // Do not use qualifiers if using `setDeviceScreen()
@Test
fun snapViewHolder() {
val activityScenario =
RobolectricActivityScenarioConfigurator.ForView()
.setDeviceScreen(DeviceScreen.Phone.PIXEL_4A)
.setLocale("en_XA")
.setUiMode(UiMode.NIGHT)
.setTheme(R.style.Custom_Theme)
.setOrientation(Orientation.PORTRAIT)
.setFontSize(FontSize.NORMAL)
.setDisplaySize(DisplaySize.NORMAL)
.launchConfiguredActivity(TRANSPARENT)
val activity = activityScenario.waitForActivity()
val layout = activity.inflateAndWaitForIdle(R.layout.memorise_row)
val viewHolder = waitForMeasuredViewHolder {
MemoriseViewHolder(
container = layout,
itemEventListener = null,
animationDelay = 0L,
).apply {
bind(generateMemoriseItem(rightAligned = false, activity = activity))
}
}
viewHolder
.itemView
.captureRoboImage("path/MemoriseViewHolder.png")
activityScenario.close()
}
}
Jetpack Compose
Here is with RobolectricActivityScenarioForComposableRule
test rule
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapComposableTest {
@get:Rule
val robolectricScreenshotRule =
RobolectricActivityScenarioForComposableRule(
config = ComposableConfigItem(
fontSize = FontSize.SMALL,
locale = "ar_XB",
uiMode = UiMode.DAY,
displaySize = DisplaySize.LARGE,
orientation = Orientation.PORTRAIT,
),
deviceScreen = DeviceScreen.Phone.PIXEL_4A,
backgroundColor = TRANSPARENT,
)
@Config(sdk = [30]) // Do not use qualifiers if using `DeviceScreen` in the Rule
@Test
fun snapComposable() {
robolectricScreenshotRule
.activityScenario
.onActivity {
it.setContent {
AppTheme { // this theme must use isSystemInDarkTheme() internally
yourComposable()
}
}
}
robolectricScreenshotRule
.composeRule
.onRoot()
.captureRoboImage("path/MyComposable.png")
}
}
The snapComposable method can be simplified further if adding the following dependency
testImplementation 'com.github.sergio-sastre.AndroidUiTestingUtils:roborazzi:<version>' // version 2.2.0+
and then capture image like this
@Test
fun snapComposable() {
robolectricScreenshotRule
.captureRoboImage("path/MyComposable.png"){
AppTheme { // this theme must use isSystemInDarkTheme() internally
yourComposable()
}
}
}
You can also use AndroidUiTestingUtils without RobolectricActivityScenarioForComposableRule
test rule as follows
@RunWith(RobolectricTestRunner::class) // or ParameterizedRobolectricTestRunner for parameterized test
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SnapComposableTest {
@get:Rule
val composeTestRule = createEmptyComposeRule()
@Config(sdk = [30]) // Do not use qualifiers if using setDeviceScreen()
@Test
fun snapComposable() {
val activityScenario =
RobolectricActivityScenarioConfigurator.ForComposable()
.setDeviceScreen(DeviceScreen.Phone.PIXEL_4A)
.setFontSize(FontSize.SMALL)
.setLocale("ar_XB")
.setInitialOrientation(Orientation.PORTRAIT)
.setUiMode(UiMode.DAY)
.setDisplaySize(DisplaySize.LARGE)
.launchConfiguredActivity(TRANSPARENT)
.onActivity {
it.setContent {
AppTheme { // this theme must use isSystemInDarkTheme() internally
yourComposable()
}
}
}
activityScenario.waitForActivity()
composeTestRule
.onRoot()
.captureRoboImage("path/MyComposable.png")
activityScenario.close()
}
}
Multiple Devices & Configs combined
AndroidUiTestingUtils also helps generate all parameters of a set of UiStates under a given set of devices and configurations. For that, use the corresponding type depending on what you are testing:
Activity:
TestDataForActivity<MyEnum>
Fragment:
TestDataForFragment<MyEnum>
Composable:
TestDataForComposable<MyEnum>
View (e.g. Dialogs, ViewHolders):
TestDataForView<MyEnum>
Here is an example with Views
@RunWith(ParameterizedRobolectricTestRunner::class)
class MultipleDevicesAndConfigsTest(
private val testItem: TestDataForView<UiStateEnum>
) {
// Define possible uiStates
enum class UiStateEnum(val value: MyUiState) {
UI_STATE_1(myUiState1),
UI_STATE_2(myUiState2),
}
// Here we generates all possible combinations
// i.e. 2 UiStates x 2 Devices x 2 Configs = 8 test
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters
fun testItemProvider(): Array<TestDataForView<UiStateEnum>> =
TestDataForViewCombinator(
uiStates = MyEnum.values()
)
.forDevices(
DeviceScreen.Phone.PIXEL_4A,
DeviceScreen.Tablet.MEDIUM_TABLET,
)
.forConfigs(
ViewConfigItem(uiMode = DAY, fontSize = SMALL),
ViewConfigItem(uiMode = NIGHT, locale = "ar"),
)
.combineAll()
}
// Passed config & device to the scenario
@get:Rule
val robolectricScreenshotRule = RobolectricActivityScenarioForViewRule(
config = testItem.config,
deviceScreen = testItem.device,
)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [30]) // Do not use qualifiers if using `DeviceScreen`in the rule
@Test
fun snapView() {
val layout = robolectricScreenshotRule.inflateAndWaitForIdle(R.layout.my_view)
val view = waitForMeasuredView {
layout.bind(item = testItem.uiState.value)
}
// testItem.screenshotId generates a unique ID for the screenshot
// based on the MyEnum.name, configuration & device name
// e.g:
// 1. UI_STATE_1_DAY_FONT_SMALL_PIXEL_4A
// 2. UI_STATE_1_DAY_FONT_SMALL_MEDIUM_TABLET
// 3. UI_STATE_1_AR_NIGHT_PIXEL_4A
// 4. UI_STATE_1_AR_NIGHT_MEDIUM_TABLET
// 5. UI_STATE_2_DAY_FONT_SMALL_PIXEL_4A
// 6,7,8. UI_STATE_2...
view.captureRoboImage("path/${testItem.screenshotId}.png")
}
}
Last updated