Simple Alarm Clock has a comprehensive test suite covering both unit tests and instrumentation tests. This guide explains how to run tests and generate code coverage reports.
Test overview
The project uses:
Unit tests : Fast tests that run on the JVM using JUnit, MockK, and AssertJ
Instrumentation tests : UI and integration tests that run on Android devices/emulators using Espresso
Code coverage : Jacoco for measuring test coverage
Running tests
Run all unit tests
Unit tests are located in app/src/test/ and run on the JVM without requiring an Android device.
Command line
Android Studio
# Run all unit tests
./gradlew test
# Run unit tests for specific variant
./gradlew testDevelopDebugUnitTest
# Run with coverage
./gradlew testDevelopDebugUnitTest jacocoTestReport
Unit tests are fast and don’t require a device. Run them frequently during development.
Run instrumentation tests
Instrumentation tests are located in app/src/androidTest/ and run on a physical device or emulator.
Connect device or start emulator
Make sure you have a device connected or an emulator running:
Run instrumentation tests
# Run all instrumentation tests
./gradlew connectedDevelopDebugAndroidTest
# Run specific test
./gradlew connectedDevelopDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.better.alarm.test.ListTest
Instrumentation tests can take 20+ minutes to complete. The project sets a timeout of 20 minutes: installation {
timeOutInMs = 20 * 60 * 1000 // 20 minutes
}
Test frameworks and libraries
Unit testing dependencies
JUnit 4
MockK
AssertJ
Coroutines Test
SLF4J Simple
testImplementation ( "junit:junit:4.13.2" )
Instrumentation testing dependencies
Espresso
AndroidX Test
AssertJ Android
androidTestImplementation ( "androidx.test.espresso:espresso-core:3.6.1" )
Example tests
Unit test example
From app/src/test/java/com/better/alarm/AlarmsTest.kt:94:
@Test
fun create () {
// when
val instance: IAlarmsManager = createAlarms ()
val newAlarm = instance. createNewAlarm ()
newAlarm. enable ( true )
// verify
store. alarms (). test (). assertValue { alarmValues ->
alarmValues.size == 1 && alarmValues[ 0 ].isEnabled
}
}
This test verifies that creating and enabling an alarm works correctly.
Instrumentation test example
From app/src/androidTest/java/com/better/alarm/test/ListTest.kt:54:
@Test
fun newAlarmShouldBeDisabledIfNotEdited () {
clickFab ()
onView ( withText ( "Cancel" )). perform ( click ())
onView ( withText ( "OK" )). perform ( click ())
assertThat ( alarmsList ()). hasSize ( 3 )
assertThat ( alarmsList (). filter { it.isEnabled }). isEmpty ()
}
This test verifies UI behavior when creating and canceling a new alarm.
Code coverage
The project uses Jacoco for code coverage reporting.
Generate coverage report
To generate a complete coverage report including both unit and instrumentation tests:
# Run all tests and generate coverage report
./gradlew test connectedDevelopDebugAndroidTest jacocoTestReport
This command:
Runs unit tests for the developDebug variant
Runs instrumentation tests on a connected device
Combines coverage data from both test types
Generates HTML and XML reports
View coverage reports
After generating the report, you can find the coverage files at:
app/build/reports/jacoco/jacocoTestReport/
├── html/
│ └── index.html # Open this in a browser
└── jacocoTestReport.xml # For CI tools like Codecov
Open app/build/reports/jacoco/jacocoTestReport/html/index.html in a web browser to view the detailed coverage report.
Coverage configuration
The Jacoco task is configured in app/build.gradle.kts:12:
tasks. create ( "jacocoTestReport" , JacocoReport:: class .java) {
group = "Reporting"
description = "Generate Jacoco coverage reports."
reports {
xml.required. set ( true )
html.required. set ( true )
}
val fileFilter = listOf (
"**/R.class" ,
"**/R$*.class" ,
"**/BuildConfig.*" ,
"**/Manifest*.*" ,
"**/*Test*.*" ,
"android/**/*.*" ,
"**/* \$\$ serializer.class" ,
)
// Combines unit and instrumentation test coverage
executionData. setFrom (
fileTree (project.buildDir) {
include (
"jacoco/testDevelopDebugUnitTest.exec" ,
"outputs/code_coverage/developDebugAndroidTest/connected/**/*.ec" ,
)
}
)
}
CI coverage reporting
The project is integrated with Codecov for tracking coverage over time.
Test configuration
Test runner configuration
The project uses AndroidX Test runner for instrumentation tests:
android {
defaultConfig {
testApplicationId = "com.better.alarm.test"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
testNamespace = "com.better.alarm.debug"
}
Unit test configuration
android {
testOptions {
unitTests.isReturnDefaultValues = true
}
}
tasks. withType (Test:: class .java) {
( this .extensions. getByName ( "jacoco" ) as JacocoTaskExtension). apply {
isIncludeNoLocationClasses = true
excludes = listOf ( "jdk.internal.*" )
}
systemProperty ( "org.slf4j.simpleLogger.logFile" , "System.out" )
systemProperty ( "org.slf4j.simpleLogger.defaultLogLevel" , "trace" )
}
Test categories
The test suite includes:
Model and business logic tests
AlarmsTest.kt - Alarm creation, scheduling, and state management
AlarmCoreTest.kt - Core alarm functionality
AlarmSchedulerTest.kt - Alarm scheduling logic
DaysOfWeekTest.kt - Days of week handling
Data and persistence tests
DataStoreAlarmsRepositoryTest.kt - DataStore persistence
PrimitiveDataStoresMediumTest.kt - DataStore operations
ProtobufTest.kt - Protocol buffer serialization
UI and integration tests
ListTest.kt - Alarm list UI interactions
PickerTest.kt - Time picker functionality
PopupTest.kt - Popup dialogs
MigrationTest.kt - Data migration scenarios
State machine tests
StateMachineTest.kt - Alarm state machine transitions
Running specific tests
Run a single test class
# Unit test
./gradlew test --tests "com.better.alarm.AlarmsTest"
# Instrumentation test
./gradlew connectedDevelopDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.better.alarm.test.ListTest
Run a single test method
# Unit test
./gradlew test --tests "com.better.alarm.AlarmsTest.create"
# Instrumentation test
./gradlew connectedDevelopDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=com.better.alarm.test.ListTest #newAlarmShouldBeDisabledIfNotEdited
Best practices
Write unit tests first
Unit tests are fast and should be your first line of defense. Write them as you develop.
Use MockK for mocking
The project uses MockK for Kotlin-friendly mocking: val stateNotifierMock: IStateNotifier = mockk (relaxed = true )
Use AssertJ for assertions
AssertJ provides fluent, readable assertions: assertThat ( alarmsList ()). hasSize ( 3 )
assertThat ( alarmsList (). filter { it.isEnabled }). isEmpty ()
Test coroutines properly
Use kotlinx-coroutines-test for testing coroutines: testImplementation ( "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" )
Run tests before committing
Always run the test suite before pushing:
Troubleshooting
Instrumentation tests fail to connect
Ensure your device/emulator is properly connected:
Out of memory during tests
Increase heap size in gradle.properties:
org.gradle.jvmargs =-Xmx2048M
Tests fail on CI but pass locally
Check that you’re using the same Gradle command as CI:
./gradlew assembleDevelopDebug
./gradlew testDevelopDebugUnitTest
Next steps
Review the contributing guidelines
Explore existing tests for examples
Check coverage reports to find untested code