The project includes comprehensive testing infrastructure for unit tests, instrumented tests, and Compose UI tests across all modules.
Testing setup
Test dependencies
The project uses industry-standard testing libraries defined in gradle/libs.versions.toml:99:
Unit testing
Instrumented testing
Compose testing
testImplementation(libs.junit) // JUnit 4.13.2
Test runner configuration
The test instrumentation runner is configured in buildSrc/src/main/java/es/mobiledev/buildsrc/AppConfig.kt:13:
const val testRunner = "androidx.test.runner.AndroidJUnitRunner"
This runner is applied to all modules through the centralized AppConfig.
Unit tests
Unit tests run on the JVM and don’t require an Android device. They execute quickly and are ideal for testing business logic.
Writing unit tests
Unit tests are located in src/test/java/. Here’s the example from app/src/test/java/es/mobiledev/cpt/ExampleUnitTest.kt:1:
package es.mobiledev.cpt
import org.junit.Assert.assertEquals
import org.junit.Test
class ExampleUnitTest {
@Test
fun addition_isCorrect () {
assertEquals ( 4 , 2 + 2 )
}
}
Running unit tests
All unit tests
Specific module
With coverage
Specific test class
Test reports
After running tests, view HTML reports at:
app/build/reports/tests/testDebugUnitTest/index.html
Unit tests are automatically run when executing ./gradlew check before builds.
Instrumented tests
Instrumented tests run on an Android device or emulator. They have access to Android framework APIs and are ideal for testing UI and platform-specific functionality.
Writing instrumented tests
Instrumented tests are located in src/androidTest/java/. Here’s the example from app/src/androidTest/java/es/mobiledev/cpt/ExampleInstrumentedTest.kt:1:
package es.mobiledev.cpt
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith (AndroidJUnit4:: class )
class ExampleInstrumentedTest {
@Test
fun useAppContext () {
// Context of the app under test.
val appContext = InstrumentationRegistry. getInstrumentation ().targetContext
assertEquals ( "es.mobiledev.cpt" , appContext.packageName)
}
}
Running instrumented tests
Connect device or start emulator
Ensure an Android device is connected or an emulator is running:
Run tests
Execute instrumented tests: ./gradlew connectedAndroidTest
View results
Check test reports at: app/build/reports/androidTests/connected/index.html
Instrumented tests require an API level 26+ device or emulator, matching the project’s minSdk = 26.
Testing specific devices
Run tests on a specific device:
adb -s DEVICE_SERIAL_NUMBER install app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
adb -s DEVICE_SERIAL_NUMBER shell am instrument -w es.mobiledev.cpt.test/androidx.test.runner.AndroidJUnitRunner
Compose UI testing
The project includes Compose UI testing dependencies for testing composable functions and UI interactions.
Setup for Compose tests
Add the Compose test rule to your instrumented test:
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith (AndroidJUnit4:: class )
class HomeScreenTest {
@get : Rule
val composeTestRule = createComposeRule ()
@Test
fun myTest () {
composeTestRule. setContent {
// Your composable here
}
// Perform assertions
composeTestRule. onNodeWithText ( "Hello" ). assertExists ()
}
}
Compose test patterns
// By text
composeTestRule. onNodeWithText ( "Click me" )
// By content description
composeTestRule. onNodeWithContentDescription ( "Profile" )
// By test tag
composeTestRule. onNodeWithTag ( "LoginButton" )
// Multiple nodes
composeTestRule. onAllNodesWithText ( "Item" )
// Existence
composeTestRule. onNodeWithText ( "Welcome" ). assertExists ()
// Display
composeTestRule. onNodeWithTag ( "LoadingSpinner" )
. assertIsDisplayed ()
// Enabled state
composeTestRule. onNodeWithText ( "Submit" )
. assertIsEnabled ()
// Text content
composeTestRule. onNodeWithTag ( "Title" )
. assertTextEquals ( "Hello World" )
Testing with Hilt
For testing components that use Hilt dependency injection:
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class HiltInstrumentedTest {
@get : Rule
var hiltRule = HiltAndroidRule ( this )
@Before
fun init () {
hiltRule. inject ()
}
@Test
fun testWithInjection () {
// Your test here
}
}
Add @HiltAndroidTest annotation and inject Hilt before running Compose tests that require dependency injection.
Test organization
Module structure
Each module follows the standard Android test structure:
module/
├── src/
│ ├── main/ # Production code
│ ├── test/ # Unit tests (JVM)
│ │ └── java/
│ └── androidTest/ # Instrumented tests (Android)
│ └── java/
Naming conventions
Follow these naming conventions for test classes:
Unit tests : [ClassName]Test.kt (e.g., ArticleRepositoryTest.kt)
Instrumented tests : [ClassName]AndroidTest.kt or [Feature]Test.kt
Compose UI tests : [Screen]ScreenTest.kt (e.g., HomeScreenTest.kt)
Test method naming
Use descriptive names that explain the test scenario:
@Test
fun `when user clicks submit button, form is validated` () {
// Test implementation
}
@Test
fun getUserById_returnsUser_whenUserExists () {
// Test implementation
}
Testing best practices
Use test fixtures
Create reusable test data:
object TestFixtures {
fun createMockArticle (id: Int = 1 ) = Article (
id = id,
title = "Test Article $id " ,
content = "Test content"
)
}
Isolate dependencies
Use test doubles for external dependencies:
class FakeArticleRepository : ArticleRepository {
private val articles = mutableListOf < Article >()
override suspend fun getArticles () = articles. toList ()
fun addArticle (article: Article ) {
articles. add (article)
}
}
Test ViewModels
Test ViewModel logic with coroutine test dispatchers:
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ArticleViewModelTest {
@OptIn (ExperimentalCoroutinesApi:: class )
@Test
fun `loadArticles updates state correctly` () = runTest {
val viewModel = ArticleViewModel (fakeRepository)
viewModel. loadArticles ()
assertEquals (LoadingState.Success, viewModel.state. value )
}
}
Continuous integration
Running all tests
Run all tests (unit + instrumented) in CI:
./gradlew check connectedAndroidTest
Test task execution
The check task runs:
All unit tests
Lint checks
KtLint checks (see Code style )
The pre-build hook ensures code quality checks run before compilation, catching issues early.
Debugging tests
Enable test logging
Add to module’s build.gradle.kts:
tasks.withType<Test> {
testLogging {
events("passed", "skipped", "failed")
showStandardStreams = true
}
}
Run single test with debugging
./gradlew :app:testDebugUnitTest --tests= "*.ExampleUnitTest" --debug-jvm
Then attach a debugger to port 5005.
Next steps
Code style Configure KtLint and maintain consistent code formatting
Building Learn about build configuration and Gradle tasks