Jetpack Compose + MVI - Part 2: Testing
Published: 2023-05-25 12:30:00

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:

{% embed https://gist.github.com/ritwikjamuar/5ba182486b3c3d5bf5a871b57ae9a0f0 %}

{% embed https://gist.github.com/ritwikjamuar/3b1b355e5c089cfa72aa90cbf0c8ded5 %}

{% embed https://gist.github.com/ritwikjamuar/b59cb17283bdbd26c9866953f13a56a1 %}

{% embed https://gist.github.com/ritwikjamuar/60de4dee8df54d64a3da11b286b7afb0 %}

{% embed https://gist.github.com/ritwikjamuar/f723518b1bacec5ae2e1a7bc4c4a7a8e %}

## 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:

{% embed https://gist.github.com/ritwikjamuar/ceef1455fb5643ef659c9efd47b72b1b %}

we add test cases like this:

{% embed https://gist.github.com/ritwikjamuar/e98baa72ad84a26faf2928556d74d8d7 %}

Note that I'm just expecting a certain `State` for my `Reducer` to transform to.

### MiddleWare

Taking our `MiddleWare` into consideration:

{% embed https://gist.github.com/ritwikjamuar/b3b4e09b6fa23f665bb22ce858abe5e7 %}

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`:

{% embed https://gist.github.com/ritwikjamuar/ea6a3d4ee214ca30ad7955e911906715 %}

Then, when creating test class for `MiddleWare`, we inject the `Store` manually with `ActionCaptureMiddleWare` as one of the `MiddleWare`.

{% embed https://gist.github.com/ritwikjamuar/7432dec3fc5d5e7912d10ded30a2333d %}

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:
```groovy
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](https://developer.android.com/jetpack/compose/semantics) 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`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction).

A `SemanticsNodeInteraction` is accessible from [`ComposeContentTestRule`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/ComposeContentTestRule).

If you see from it's documentation, ComposeContentTestRule implements [`SemanticsNodeInteractionsProvider`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/SemanticsNodeInteractionsProvider) which contains method that provides the `SemanticsNodeInteraction`.

Now, to get hold of a particular `SemanticsNodeInteraction`, we must provide a [`SemanticMatcher`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/SemanticsMatcher) to the method [`onNode()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/SemanticsNodeInteractionsProvider#onNode(androidx.compose.ui.test.SemanticsMatcher,kotlin.Boolean)) 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:

![Compose Testing Cheat-Sheet](https://developer.android.com/images/jetpack/compose/compose-testing-cheatsheet.png)

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.

```kotlin
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`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#%28androidx.compose.ui.Modifier%29.testTag%28kotlin.String%29). `testTag` is one of the attributes of [`Modifier`](https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier#%28androidx.compose.ui.Modifier). It means, `testTag` can be added to almost any `@Composable`, especially the ones we want to test on.

```kotlin
fun testCase() {
    rule.apply {

        ...

        onNodeWithTag(testTag = "SOME_TEST_TAG").apply {
            ... // Perform assertions
        }

    }
}
```

So, refactored the Screen to use `testTag` like below:

{% embed https://gist.github.com/ritwikjamuar/bbe80e24a455d4e53e47a4ead14ea793 %}

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`.

```kotlin
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.

```kotlin
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:

{% embed https://gist.github.com/ritwikjamuar/66eedab82b08680a06dbd9bf2d4b712f %}

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:

{% embed https://github.com/ritwikjamuar/ComposeSample %}

Thank You!