Android Coden
Android 7 min lesen

Kotlin Testing Basics

Kleine Unit-Tests prüfen Kotlin-Logik früh und zuverlässig. Du lernst, wie JUnit und Assertions deinen Android-Code absichern.

Kotlin Testing Basics bedeutet: Du schreibst kleine, schnelle Tests für Kotlin-Code, der ohne Android-Framework funktioniert. Damit prüfst du Regeln, Berechnungen, Formatierungen, Mapper, Validatoren oder Zustandslogik, bevor daraus ein sichtbarer Fehler in deiner App wird. In Android-Projekten ist das besonders wertvoll, weil nicht jede Logik einen Emulator, eine Activity oder eine Compose-Oberfläche braucht, um sinnvoll geprüft zu werden.

Was ist das?

Ein Unit-Test prüft eine kleine Einheit deines Codes isoliert. Diese Einheit kann eine Funktion, eine Klasse oder ein klar abgegrenztes Verhalten sein. Bei Kotlin Testing Basics geht es nicht darum, die komplette App zu starten. Du willst stattdessen beantworten: Wenn ich diesen Input gebe, liefert meine Kotlin-Logik dann den erwarteten Output?

Das passt gut zu modernem Android, weil Apps heute oft in Schichten aufgebaut sind: UI mit Compose, Zustand in ViewModels, Datenzugriff in Repositories und Fachlogik in eigenen Klassen oder Use Cases. Je weniger eine Klasse direkt von Context, Resources, Activity, View, Datenbank oder Netzwerk abhängt, desto leichter kannst du sie als lokalen Unit-Test prüfen. Diese Tests laufen auf der JVM deines Entwicklungsrechners und sind deshalb deutlich schneller als instrumentierte Android-Tests auf Gerät oder Emulator.

Das mentale Modell ist einfach: Ein Test ist ein kleines Experiment mit klarer Erwartung. Du bereitest Daten vor, rufst Code auf und prüfst das Ergebnis mit einer Assertion. Wenn die Assertion fehlschlägt, zeigt dir der Test, welche Annahme nicht mehr stimmt. Für Anfänger ist wichtig: Tests sind keine Zusatzaufgabe nach dem „eigentlichen“ Programmieren. Sie sind ein Werkzeug, mit dem du Verhalten beschreibst und spätere Änderungen sicherer machst.

In realen Android-Teams erscheinen solche Tests im Alltag ständig. Du testest zum Beispiel, ob ein Passwort-Validator die richtigen Fehlermeldungen liefert, ob ein Preis korrekt formatiert wird, ob ein Mapper eine API-Antwort in ein UI-Modell umwandelt oder ob eine Zustandsentscheidung bei leeren Daten korrekt reagiert. Solche Regeln sind oft klein, aber sie tragen viel Produktqualität. Wenn sie nur manuell in der App geprüft werden, werden Fehler spät und teuer sichtbar.

Wie funktioniert es?

Die typische Basis besteht aus JUnit und Assertions. JUnit ist das Test-Framework, das deine Testmethoden findet, ausführt und Ergebnisse meldet. Assertions sind Prüfungen wie „dieser Wert muss gleich jenem Wert sein“ oder „dieser Ausdruck muss wahr sein“. In Kotlin kannst du JUnit direkt verwenden. In Android-Projekten liegen lokale Unit-Tests meistens unter src/test, während instrumentierte Tests unter src/androidTest liegen. Für dieses Thema ist src/test der zentrale Ort.

Ein Test folgt meist dem Muster Arrange, Act, Assert. Bei Arrange bereitest du die Eingaben und das Testobjekt vor. Bei Act rufst du die Funktion auf, die du prüfen willst. Bei Assert vergleichst du das tatsächliche Ergebnis mit der Erwartung. Dieses Muster ist kein Pflichtcode, aber es hilft dir, Tests lesbar zu halten. Lesbarkeit ist wichtig, weil Tests oft Monate später erklären müssen, warum sich Code auf eine bestimmte Weise verhalten soll.

JUnit markiert Tests mit @Test. Eine Testklasse ist meist eine normale Kotlin-Klasse. Der Testname sollte verständlich sagen, welches Verhalten geprüft wird. In Kotlin sind Namen mit Backticks erlaubt, dadurch kannst du Tests als kurze Sätze schreiben. Das ist nützlich, solange dein Team diesen Stil akzeptiert und die Namen nicht zu lang werden.

Eine Assertion ist der Kern des Tests. Mit assertEquals(expected, actual) prüfst du Gleichheit. Mit assertTrue(...) oder assertFalse(...) prüfst du Bedingungen. Wichtig ist die Reihenfolge: Viele JUnit-Assertions erwarten zuerst den erwarteten Wert und danach den tatsächlichen Wert. Wenn du das vertauschst, läuft der Test zwar, aber Fehlermeldungen werden verwirrend.

Der entscheidende Punkt im Android-Kontext ist die Trennung von reiner Kotlin-Logik und Android-Abhängigkeiten. Ein lokaler Unit-Test kann ohne zusätzliche Hilfen keine echte Activity, keinen echten Context und keine echten Android-Framework-Klassen ausführen. Wenn deine Fachlogik überall direkt Android APIs verwendet, wird sie schwer testbar. Deshalb ist Testbarkeit auch ein Architektur-Signal: Code, der klare Eingaben und Ausgaben hat, ist leichter zu verstehen, leichter zu ändern und leichter zu prüfen.

Das bedeutet nicht, dass du Android APIs nie testen darfst. Es bedeutet nur, dass du den passenden Testtyp wählen musst. Für Kotlin Testing Basics bleibt die Grenze bewusst eng: kleine Tests, die Kotlin-Logik ohne Android-Framework prüfen. UI-Tests, Compose-Tests, Datenbanktests oder Netzwerk-Integrationstests haben ihren eigenen Platz. Hier lernst du zuerst das Fundament, damit spätere Testarten nicht wie ein unübersichtlicher Werkzeugkasten wirken.

In der Praxis

Angenommen, du baust eine Registrierungsmaske. Die Compose-Oberfläche ist nicht der beste erste Ort, um die Passwortregel zu testen. Die Regel selbst kann als reine Kotlin-Funktion oder kleine Klasse existieren. Dann prüfst du sie mit lokalen Unit-Tests.

class PasswordValidator {
    fun isValid(password: String): Boolean {
        return password.length >= 8 &&
            password.any { it.isDigit() } &&
            password.any { it.isLetter() }
    }
}

Dazu passt ein JUnit-Test:

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class PasswordValidatorTest {

    private val validator = PasswordValidator()

    @Test
    fun `valid password contains letters digits and enough characters`() {
        val result = validator.isValid("android8")

        assertTrue(result)
    }

    @Test
    fun `password without digit is invalid`() {
        val result = validator.isValid("androidx")

        assertFalse(result)
    }

    @Test
    fun `short password is invalid`() {
        val result = validator.isValid("a1b2")

        assertFalse(result)
    }
}

Dieser Test prüft nicht, ob ein Textfeld korrekt angezeigt wird. Er prüft nur die Regel. Genau das ist hier der Vorteil. Wenn du später das Compose-Layout änderst, bleiben diese Tests stabil. Wenn du die Passwortregel änderst, zeigen dir die Tests sofort, welche Fälle betroffen sind.

Eine gute Entscheidungsregel lautet: Wenn du ein Verhalten als Funktion mit Eingabe und Ausgabe formulieren kannst, beginne mit einem lokalen Unit-Test. Beispiele sind Sortierung, Filterung, Berechnung, Validierung, Mapping und Zustandsauswahl. Wenn du dagegen prüfen willst, ob ein Button sichtbar ist oder ob eine Activity mit dem System interagiert, brauchst du eine andere Testebene.

Eine typische Stolperfalle ist, private Implementierungsdetails zu testen. Wenn du Tests so schreibst, dass sie jeden internen Zwischenschritt kennen, brechen sie bei harmlosen Refactorings. Teste lieber beobachtbares Verhalten. Im Beispiel interessiert nicht, wie any { it.isDigit() } intern verwendet wird. Wichtig ist, dass Passwörter ohne Ziffer abgelehnt werden und gültige Passwörter akzeptiert werden.

Eine zweite Stolperfalle ist zu viel Logik in ViewModels oder Composables zu lassen. Wenn eine Compose-Funktion entscheidet, welche Fachregel gilt, wird der Test schnell schwerer als nötig. Ziehe reine Regeln in kleine Kotlin-Klassen oder Funktionen. Das macht nicht nur Tests einfacher, sondern auch Code-Reviews klarer: Andere Entwickler sehen schneller, welche Regel unabhängig von der UI gilt.

Auch Fehlermeldungen sind Teil der Praxis. Ein schwacher Test prüft nur true oder false, obwohl die App später eine konkrete Fehlermeldung braucht. In solchen Fällen kann ein Ergebnistyp besser sein:

sealed interface PasswordResult {
    data object Valid : PasswordResult
    data object TooShort : PasswordResult
    data object MissingDigit : PasswordResult
}

class PasswordRules {
    fun validate(password: String): PasswordResult {
        if (password.length < 8) return PasswordResult.TooShort
        if (password.none { it.isDigit() }) return PasswordResult.MissingDigit
        return PasswordResult.Valid
    }
}

Der Test wird dadurch aussagekräftiger:

import org.junit.Assert.assertEquals
import org.junit.Test

class PasswordRulesTest {

    private val rules = PasswordRules()

    @Test
    fun `short password returns too short result`() {
        val result = rules.validate("abc1")

        assertEquals(PasswordResult.TooShort, result)
    }

    @Test
    fun `password without digit returns missing digit result`() {
        val result = rules.validate("androidx")

        assertEquals(PasswordResult.MissingDigit, result)
    }
}

Hier siehst du, wie Tests Design beeinflussen können. Der Code wird nicht komplizierter, sondern klarer: Statt eines nackten Boolean-Werts bekommt deine App eine fachliche Antwort. Das hilft ViewModel und UI, die passende Meldung anzuzeigen, ohne die Regel erneut zu interpretieren.

Für die tägliche Arbeit solltest du Tests klein halten. Ein Test sollte möglichst nur einen Grund haben, fehlzuschlagen. Wenn ein Test gleichzeitig Passwortlänge, Netzwerkstatus, Datenbankzustand und UI-Text prüft, ist er kein guter Unit-Test. Er verrät dir bei einem Fehler nicht schnell genug, was kaputt ist. Schreibe lieber mehrere kleine Tests mit klaren Namen.

Wenn du in einem Team arbeitest, gehören Unit-Tests auch in die Continuous-Integration-Pipeline. Dort laufen sie bei Pull Requests automatisch. Lokale Unit-Tests sind dafür gut geeignet, weil sie schnell sind und keine Geräte benötigen. Sie ersetzen kein manuelles Ausprobieren und keine höheren Testebenen, aber sie geben früh Feedback. Gerade bei Junior-Entwicklern ist das hilfreich, weil du nicht erst im Review erfährst, dass eine kleine Regel unbeabsichtigt geändert wurde.

Fazit

Kotlin Testing Basics geben dir ein solides Fundament für verlässlichen Android-Code: Du trennst reine Logik von Android-Abhängigkeiten, formulierst erwartetes Verhalten mit JUnit und prüfst es mit klaren Assertions. Übe das an einer kleinen Klasse aus deinem Projekt: Suche eine Validierung, einen Mapper oder eine Berechnung, schreibe drei Unit-Tests für typische Fälle und einen Randfall, führe sie lokal aus und lies die Fehlermeldungen bewusst. Danach prüfe im Code-Review, ob deine Tests Verhalten beschreiben oder nur interne Schritte festnageln.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.