Testing Jetpack Compose without emulator or device
Piotr Chmielowski
Posted on November 17, 2021
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"
We need also JUnit library:
testImplementation 'junit:junit:4.13.2'
And Robolectric. Robolectric is a test runner which provides Android SDK dependencies for pure JVM tests:
testImplementation 'org.robolectric:robolectric:4.7'
Robolectric also requires the following line in the Gradle configuration:
testOptions {
unitTests {
includeAndroidResources = true
}
}
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"
}
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
}
Writing test
To use Robolectric, we need to add the following annotation to the test class:
@RunWith(RobolectricTestRunner::class)
To test composables we need to add the following test rule:
@get:Rule
val rule = createComposeRule()
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()
}
}
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")
}
}
Posted on November 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.