Testing Jetpack Compose without emulator or device
Tags: android, jetpackcompose, robolectric, testing
Published: 2021-11-17 17:31:43

How to verify code which uses Jetpack Compose with pure JVM tests running on the developer's PC - without emulator or physical Android device.

## Gradle setup
First requirement is to add Jetpack Compose testing artifacts:
```groovy
testImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
```

We need also JUnit library:
```groovy
testImplementation 'junit:junit:4.13.2'
```

And Robolectric. Robolectric is a test runner which provides Android SDK dependencies for pure JVM tests:
```groovy
testImplementation 'org.robolectric:robolectric:4.7'
```

Robolectric also requires the following line in the Gradle configuration:
```groovy
testOptions {
    unitTests {
        includeAndroidResources = true
    }
}
```

Here is a final `app/build.gradle` file:
```groovy
plugins {
    id 'com.android.application'
    id 'kotlin-android'
}
android {
    compileSdk 31
    defaultConfig {
        applicationId "net.chmielowski.testingcompose"
        minSdk 28
        targetSdk 31
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
    }
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}
dependencies {
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"

    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.robolectric:robolectric:4.7'
    testImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
}
```

And project level `build.gradle` file:
```groovy
buildscript {
    ext {
        compose_version = '1.0.5'
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.3"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
```

## Writing test
To use Robolectric, we need to add the following annotation to the test class:
```kotlin
@RunWith(RobolectricTestRunner::class)
```

To test composables we need to add the following test rule:
```kotlin
@get:Rule
val rule = createComposeRule()
```

There is also an [issue with Robolectric](https://github.com/robolectric/robolectric/issues/6593) which can be worked around by adding `@Config(instrumentedPackages = ["androidx.loader.content"])` annotation to the test class. 

Putting it all together, the basic test could look like the following:
```groovy
package net.chmielowski.testingcompose

import androidx.compose.material.Text
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(instrumentedPackages = ["androidx.loader.content"])
class ExampleUnitTest {

    @get:Rule
    val rule = createComposeRule()

    @Test
    fun basicTestCase() {
        rule.setContent { Text("Hello, World!") }
        rule
            .onNodeWithText("Hello, World!")
            .assertExists()
    }
}
```

## More advanced test

Here is an example of the slightly more advanced test:

```kotlin
package net.chmielowski.testingcompose

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(instrumentedPackages = ["androidx.loader.content"])
class ExampleUnitTest {

    @get:Rule
    val rule = createComposeRule()

    @Test
    fun basicTestCase() {
        rule.setContent {
            var number by remember { mutableStateOf(0) }
            Text(
                text = number.toString(),
                modifier = Modifier.testTag("Counter"),
            )
            Button(onClick = { number++ }) {
                Text("Increment")
            }
        }
        rule
            .onNodeWithTag("Counter")
            .assertTextEquals("0")
        rule
            .onNodeWithText("Increment")
            .performClick()
        rule
            .onNodeWithTag("Counter")
            .assertTextEquals("1")

    }
}
```