๐Ÿงช Testing, Debugging, and Performance

Unit Testing in Android with JUnit

Unit tests are the cheapest, fastest safety net an Android app can have. They run on your computer in seconds and catch regressions before users do. This guide covers what to unit test, how to structure tests, and the habits that keep them valuable.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Beginner
On this page
  1. Where unit tests fit
  2. What to unit test
  3. Setup
  4. A well-structured test
  5. Fakes, stubs and mocks
  6. Testing many cases
  7. When code touches Android classes
  8. Running tests
  9. High-value tests
  10. Common mistakes
  11. Frequently asked questions
  12. Conclusion

Many Android developers skip tests because UI tests felt slow and flaky. Unit tests are different: they test plain Kotlin logic (ViewModels, mappers, validators, reducers) on the JVM, with no device needed. A few hundred of them run in seconds.

Where unit tests fit

Android test types by speed and scope Local unit tests in src/test run on the JVM in milliseconds and are the majority. Instrumented tests in src/androidTest run on a device or emulator and cover integration and UI. End-to-end tests are fewest and slowest. E2E (few) Instrumented: src/androidTest (device) Local unit tests: src/test (JVM, ms each)
Most of your tests should be fast local unit tests. Keep device tests for what truly needs Android.

What to unit test

  • ViewModels: state changes in response to events and data.
  • Business logic: validators, calculators, use cases, reducers.
  • Mappers: DTO to domain to UI conversions, especially edge cases.
  • Repositories: caching and merging logic, with fake data sources.

Don't unit test framework code (Room's SQL is better tested with an in-memory DB in instrumented tests) or trivial getters.

Setup

app/build.gradle.ktsGradle KTS
dependencies {
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>")
    testImplementation("com.google.truth:truth:<version>")       // optional: readable assertions
}

A well-structured test

src/main/.../PasswordValidator.ktKotlin
object PasswordValidator {
    sealed interface Result {
        data object Valid : Result
        data class Invalid(val reasons: List<String>) : Result
    }

    fun validate(password: String): Result {
        val reasons = buildList {
            if (password.length < 8) add("At least 8 characters")
            if (password.none(Char::isDigit)) add("At least one number")
            if (password.none(Char::isUpperCase)) add("At least one uppercase letter")
        }
        return if (reasons.isEmpty()) Result.Valid else Result.Invalid(reasons)
    }
}
src/test/.../PasswordValidatorTest.ktKotlin
class PasswordValidatorTest {

    @Test
    fun `valid password passes`() {
        assertEquals(PasswordValidator.Result.Valid, PasswordValidator.validate("Secure123"))
    }

    @Test
    fun `short password without digits lists both reasons`() {
        // Given a short, letters-only password
        val password = "Abc"
        // When validated
        val result = PasswordValidator.validate(password)
        // Then both problems are reported
        assertEquals(
            PasswordValidator.Result.Invalid(listOf("At least 8 characters", "At least one number")),
            result,
        )
    }
}

Backtick names describe behaviour in plain language, and the given/when/then structure keeps tests readable.

Fakes, stubs and mocks

DoubleWhat it isWhen
FakeA working lightweight implementation (in-memory repository)Default choice for repositories and data sources
StubReturns canned valuesSimple dependencies
MockRecords and verifies calls (MockK, Mockito)Verifying interactions such as "analytics logged once"

Google's testing guidance favours fakes where practical. They make tests describe behaviour rather than implementation details. See MockK vs Mockito.

Testing many cases

Table-driven testKotlin
@Test
fun `formats durations`() {
    val cases = mapOf(0L to "0:00", 59L to "0:59", 61L to "1:01", 3_600L to "60:00")
    cases.forEach { (seconds, expected) ->
        assertEquals("for $seconds s", expected, formatDuration(seconds))
    }
}

When code touches Android classes

Local tests run on the JVM with a stub android.jar, so Android framework calls throw by default. Options:

  • Design it away: keep logic in classes without Android dependencies (best).
  • Inject abstractions: for example a StringProvider or Clock instead of Context or System.currentTimeMillis().
  • Robolectric: runs Android framework code on the JVM when you really need it.

Running tests

TerminalShell
./gradlew testDebugUnitTest                 # all local unit tests for the debug variant
./gradlew :feature:cart:testDebugUnitTest   # one module
# HTML report: <module>/build/reports/tests/testDebugUnitTest/index.html

High-value tests

Money and dates

Rounding, currency formatting, time zones. Bugs here are costly and easy to test.

State transitions

Reducers and ViewModels: loading to success, error to retry.

Bug regressions

Every fixed bug gets a test that would have caught it.

Common mistakes

  • Testing implementation details (private methods, call order) so every refactor breaks tests.
  • Using real time, randomness or the network in unit tests.
  • Huge setup shared by unrelated tests. Prefer small builders and fakes.
  • No assertions, or assertions that can't fail.

Frequently asked questions

JUnit 4 or JUnit 5?

Android's tooling and many AndroidX test libraries are built around JUnit 4, which is the simplest default. JUnit 5 can be used for local tests with additional setup.

How much coverage should I aim for?

Coverage is a weak target. Aim to cover business logic and state transitions thoroughly, and use coverage reports to find untested risky code.

Can AI write my tests?

AI tools can draft tests, but you must check that they assert meaningful behaviour. See AI-Assisted Unit Test Generation.

Conclusion

Unit tests are fast, cheap and high-value when they target logic: ViewModels, validators, mappers and use cases. Keep logic free of Android classes, prefer fakes, name tests by behaviour, and run them on every commit.

Next step: test asynchronous code properly in Testing ViewModels with Coroutines and Flow.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Performance work starts with understanding how Android apps are built and how they behave at runtime. Strengthen those fundamentals and the profiling tools start to make sense.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp