Previously, I had discussed how to handle any side effect that led us to discover what purpose a MiddleWare
and Reducer
should carry on.
Now, it's time to do some testing of our code. But before we proceed, it's good to take an overview of code we've incorporated so far:
Unit Testing
In MVI + Redux Design Pattern, we can Unit Test following:
-
Reducer
: Test out the UI State Management. -
MiddleWare
: Test out the side-effects.
Reducer
Testing Reducer
is straightforward: We assert that for a given current State
of UI and an Action
supplied, when reduced should churn out the State
we expect.
Taking our below Reducer
into consideration:
we add test cases like this:
Note that I'm just expecting a certain State
for my Reducer
to transform to.
MiddleWare
Taking our MiddleWare
into consideration:
Testing MiddleWare
is tricky: The method process()
of MiddleWare
does not return anything.
However, we can test what Action
s our MiddleWare
propagates back to Store
. This testing for action itself can be thought of as a side-effect.
So, we create a general purpose MiddleWare
that captures Action
dispatched from Store
:
Then, when creating test class for MiddleWare
, we inject the Store
manually with ActionCaptureMiddleWare
as one of the MiddleWare
.
Finally, we assert using ActionCaptureMiddleWare
to check if a certain Action
is captured or should not be captured Action
.
Instrumentation Testing
In this testing, the focus area is Views. Objective for testing UI is to test for a given Test Case, UI should behave as expected.
UI Test in Jetpack Compose
Jetpack Compose testing libraries allows us to Test an individual @Composable
.
Library in highlight:
dependencies {
androidTestImplementation platform('androidx.compose:compose-bom:2023.05.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
}
All views in Jetpack Compose are represented as their View Tree. Beside view, there exists a Semantic Tree in parallel with View Tree. This Semantic Tree is useful in Android Framework as it is used by Accessibility Services and Testing frameworks.
Here, each node in this Semantic Tree is a semantic image of same view from View Tree and is represented by SemanticsNodeInteraction
.
A SemanticsNodeInteraction
is accessible from ComposeContentTestRule
.
If you see from it's documentation, ComposeContentTestRule implements SemanticsNodeInteractionsProvider
which contains method that provides the SemanticsNodeInteraction
.
Now, to get hold of a particular SemanticsNodeInteraction
, we must provide a SemanticMatcher
to the method onNode()
of ComposeContentTestRule
.
Upon getting hold of a SemanticsNodeInteraction
, then we can perform assertions on it. One example of assertion in SemanticsNodeInteraction
is whether the view is actually shown in the UI or not, or if view is clickable, and such.
Here's the Cheat-Sheet for combining SemanticMatcher
s and Finder methods:
Finally, before start performing testing, we have to populate the @Composable
under setContent()
method of ComposeContentTestRule
.
UI Test in Action
Before we start our test, fetching the UI element should be figured out, remember, @Composable
does not use id
like XMLs do.
One way is to use hasText(text = ?) : SemanticsMatcher
which provides all SemanticsNodeInteraction
that matches our supplied text.
fun testCase() {
rule.apply {
...
onNode(hasText("TEXT_TO_FIND")).apply {
... // Perform assertions
}
}
}
This works, but is not reliable and can prove cumbersome especially if the supplied text is displayed over more than one @Composable
.
A better approach I find useful is to use testTag
. testTag
is one of the attributes of Modifier
. It means, testTag
can be added to almost any @Composable
, especially the ones we want to test on.
fun testCase() {
rule.apply {
...
onNodeWithTag(testTag = "SOME_TEST_TAG").apply {
... // Perform assertions
}
}
}
So, refactored the Screen to use testTag
like below:
Now, before implementing the test, here's the formula for performing test on any composable, which makes use of ComposeContentTestRule
:
STEP 1: Show the Composable UI under the method setContent
of ComposeContentTestRule
.
class SomeUITest {
@get: Rule
private val rule: ComposeContentTestRule = createComposeRule()
fun testCase() {
rule.apply {
setContent {
SomeScreen()
}
...
}
}
}
STEP 2: Using ComposeContentTestRule
, get hold of any SemanticsNodeInteraction
using testTag
to perform assertions on it.
class SomeUITest {
@get: Rule
private val rule: ComposeContentTestRule = createComposeRule()
fun testCase() {
rule.apply {
setContent {
...
}
view().apply {
... // Perform assertions here
}
}
}
private fun ComposeContentTestRule.view(): SemanticsNodeInteraction =
onNodeWithTag(testTag = "SOME_EXAMPLE_TAG")
}
With above steps in mind, this is how Test Class for ExampleScreen
is implemented:
Notice that I have created another Composable TestScreen. This is done because our ExampleScreen
is implemented best when coupled with ExampleViewModel
. And when we use MVIViewModel
, we get viewState
, which get collectAsState()
. So anytime ExampleReducer
changes the ExampleState
, ExampleScreen
is going to be recomposed. So wrapped ExampleScreen
in another Composable to avoid recomposition and retain the same instance of MVIViewModel
.
Takeaways
With MVI + Redux, Unit Testing the logical components:
-
Reducer
can be tested for their role in changing theState
of UI. -
MiddleWare
can be tested for their propagation ofAction
toStore
, giving a view on what are they supposed to propagate.
With Jetpack Compose UI Testing, any @Composable
can be tested for correctness, leading to a more bug-free UIs.
With this, I conclude the series.
Here's link to my implementation of all Tutorials:
ritwikjamuar / ComposeSample
Experimentation of screens with Jetpack Compose.
Compose Sample
Experimentation of screens with Jetpack Compose.
Description
This project contains some very basic UI screens (Login for example with just barebones). While UI may be simplistic, most detail to attention has been paid on Design Pattern and Testing (Both Unit & Instrumentation) This project is also my first exposure to Jetpack Compose.
Chapters
License
MIT License
Copyright (c) 2023 Ritwik Jamuar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright
…Thank You!
Top comments (0)