Blog Infos
Author
Published
Topics
, , ,
Published

This is the first of three articles about Jetpack Compose UI Testing that I will be publishing in the next few weeks. Within these articles, I will explain some of the available UI Testing options to apply in production projects. I will begin each article with an introduction into some basic/general aspects, from the most practical perspective.

First of all, let me present a very concise overview without being too repetitive (there is a lot of information on the main Android site, if you wish to check all of the general details).

  • Jetpack Compose uses semantics to give meaning to a piece of UI, apart from that, it’s primarily used for accessibility.
Compose UI Tests using Testing in Isolation 🔬

One of the biggest advantages of using Jetpack Compose is that it allows you to start an activity displaying any composable: your full application, a single screen, or a small element.

  • It allows you to check that composables are correctly encapsulated and they work independently, allowing for easier and more focused UI testing.

You might have defined, in your EntryPointActivity, something like this:

setContent {
SpaceXTheme {
val bottomNavigationItems = listOf(
BottomNavigationScreens.Dashboard,
BottomNavigationScreens.Launches
)
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation(navController, bottomNavigationItems)
}
) {
NavHost(navController, startDestination = BottomNavigationScreens.Dashboard.route) {
composable(BottomNavigationScreens.Dashboard.route) {
InitDashboardScreen()
}
composable(BottomNavigationScreens.Launches.route) {
InitLaunchesScreen()
}
}
}
}

As you can see, there’s a BottomNavigation (with two navigation items) and therefore two tabs: DashboardScreen and LaunchesScreen. But you may not want to test the whole UI structure defined in the Activity. Instead, you can simply test one of the tabs directly (specific screen):

@Test
@InternalCoroutinesApi
fun elementsVisibilityAfterTwoItemsRetrieved() {
composeTestRule.apply {
setContent {
SpaceXTheme {
LaunchesScreen(
state = LaunchesContract.State(
listOf(
LaunchUiModel(
"Mission1", "08-12-2021", true, "0", RocketUiModel(
"Rocket1", "Rocket Type1"
), LinksUiModel("", "", "Youtube Link")
),
LaunchUiModel(
"Mission2", "09-12-2021", false, "0", RocketUiModel(
"Rocket2", "Rocket Type2"
), LinksUiModel("", "WikiPedia Link", "Youtube Link")
)
), isLoading = false, isError = false
),
effectFlow = flow { emit(LaunchesContract.Effect.ClickableLink.None) } )
}
}
onNodeWithText("Mission1", useUnmergedTree = true)
.assertIsDisplayed()
onNodeWithText("Mission2", useUnmergedTree = true)
.assertIsDisplayed()
onAllNodesWithContentDescription(
"Item",
substring = true,
useUnmergedTree = true
).assertCountEquals(numItemsShowed)
}
}

The test is focused on one screen,
rather than in the whole Activity content

 

LaunchesScreen is the one set in the content (by setContent) and the only UI State specified is the one for that screen too.

Progressively it’s possible to specify smaller parts of the UI and test all of them in isolation to check any specific case.

Compose UI Tests using MockWebServer ⚡️

MockWebServer is a library from Square that allows you to specify which responses to return and then verify that requests were made as expected.

Advantages
  • Tests using MockWebServer can coexist with the ones using Testing in Isolation.
Configuration
  1. Add the next dependency into the build.gradle (app level)
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.3")

2. Define a MockTestRunner and link it through your build.gradle (app)

android {
defaultConfig {
applicationId = "prieto.fernando.sample"
testInstrumentationRunner = "prieto.fernando.sample.webmock.MockTestRunner"
}
buildFeatures {
compose = true
viewBinding = true
}
// rest of configuration
}

MockTestRunner needs to use a TestApplication class, which is conveniently provided by Dagger Hilt:

class MockTestRunner : AndroidJUnitRunner() {
override fun onCreate(arguments: Bundle?) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder().permitAll().build())
super.onCreate(arguments)
}
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

Job Offers

Job Offers


    Kotlin Multiplatform Mobile Developer

    Touchlab
    Remote
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

,

Overview of UI testing in Compose

Jetpack Compose has no views, so how do we test it? Come learn about the basics of UI testing in Jetpack Compose.
Watch Video

Overview of UI testing in Compose

Michael Bailey
Distinguished Engineer
American Express

Overview of UI testing in Compose

Michael Bailey
Distinguished Engine ...
American Express

Overview of UI testing in Compose

Michael Bailey
Distinguished Engineer
American Express

Jobs

3. Create a FakeNetworkModule to replace the original one (NetworkModule) in the tests configuration.

Let’s define our baseUrl in a way that can be overridden in the fake module:

@InstallIn(SingletonComponent::class)
@Module
open class NetworkModule {
open fun getBaseUrl () ="https://api.spacexdata.com/v3/"
@Provides
@BaseUrl
fun provideBaseUrl() = getBaseUrl ()
// The rest of Builders, Factories, etc. are omitted for simplicity
}

Hilt provides a very intuitive and simple solution to replace modules, for testing purposes. By simply pointing to the target module that’s being overridden and selecting the local host that MockWebServer needs to point to, it’s possible to get this network setup done. The Port can be 8080 defined in the build.gradle file.

@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [NetworkModule::class]
)
class FakeNetworkModule : NetworkModule() {
override fun getBaseUrl() = "http://127.0.0.1:${BuildConfig.PORT}"
}

4. Define your own Dispatchers.

By default MockWebServer uses a queue to specify a series of responses. Use a Dispatcher to handle requests using another policy. One natural policy is to dispatch on the request path. You can, for example, filter the request instead of using server.enqueue().

A common practice is to define a SuccessDispatcher and an ErrorDispatcher. Those two classes will deliver the specified responses in each case (success & error) and will be obtained from the path indicated.

Once the path to the expected responses is specified (JSON files), MockWebServer will be able to parse the content and offer it automatically in your tests.

 Responses files defined within the project(debug directory, same level as androidTest)

5. Initialise MockWebServer in your tests.

val mockWebServer by lazy { MockWebServer() }
@Before
fun setUp() {
mockWebServer.start(BuildConfig.PORT)
}
@After
fun teardown() {
mockWebServer.shutdown()
}
view raw BaseTest.kt hosted with ❤ by GitHub

6. Start testing using MockWebServer.

@ExperimentalMaterialApi
@RunWith(AndroidJUnit4::class)
@HiltAndroidTest
class LaunchesScreenKtTest : BaseScreenTest() {
@Test
@InternalCoroutinesApi
fun visibleItemsCountAfterOpeningTheScreen() {
mockWebServer.dispatcher = SuccessDispatcher()
setMainContent()
composeTestRule.apply{
onNodeWithText("Launches").assertIsDisplayed().performClick()
mainClock.advanceTimeBy(2000)
onAllNodesWithContentDescription(
"Item",
substring = true,
useUnmergedTree = true
).assertCountEquals(6)
}
}
}

If you need to check more details and implement the whole configuration I have put in place, make sure you check my Github example SpaceX prepare for Clean Architecture liftoff🚀.

References

Jake Wharton Blog

Arunkumar Blog

Jetpack Compose Official

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE

Leave a Reply

Your email address will not be published.

Fill out this field
Fill out this field
Please enter a valid email address.

Menu