Testing Jetpack Compose without emulator or device

pchmielowski

Piotr Chmielowski

Posted on November 17, 2021

Testing Jetpack Compose without emulator or device

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:

testImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
Enter fullscreen mode Exit fullscreen mode

We need also JUnit library:

testImplementation 'junit:junit:4.13.2'
Enter fullscreen mode Exit fullscreen mode

And Robolectric. Robolectric is a test runner which provides Android SDK dependencies for pure JVM tests:

testImplementation 'org.robolectric:robolectric:4.7'
Enter fullscreen mode Exit fullscreen mode

Robolectric also requires the following line in the Gradle configuration:

testOptions {
    unitTests {
        includeAndroidResources = true
    }
}
Enter fullscreen mode Exit fullscreen mode

Here is a final app/build.gradle file:

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

And project level build.gradle file:

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

Writing test

To use Robolectric, we need to add the following annotation to the test class:

@RunWith(RobolectricTestRunner::class)
Enter fullscreen mode Exit fullscreen mode

To test composables we need to add the following test rule:

@get:Rule
val rule = createComposeRule()
Enter fullscreen mode Exit fullscreen mode

There is also an issue with Robolectric 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:

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

More advanced test

Here is an example of the slightly more advanced test:

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

    }
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
pchmielowski
Piotr Chmielowski

Posted on November 17, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related