DEV Community

Ritwik Jamuar
Ritwik Jamuar

Posted on

Jetpack Compose + MVI - Part 2: Testing

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 Actions 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'
}
Enter fullscreen mode Exit fullscreen mode

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 SemanticMatchers and Finder methods:

Compose Testing Cheat-Sheet

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
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }

    }
}
Enter fullscreen mode Exit fullscreen mode

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()
            }

            ...

        }
    }

}
Enter fullscreen mode Exit fullscreen mode

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")

}
Enter fullscreen mode Exit fullscreen mode

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 the State of UI.
  • MiddleWare can be tested for their propagation of Action to Store, 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:

GitHub logo 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)