Testing Mindset: Verhalten prüfen, Regressionen vermeiden
Tests zeigen, ob wichtiges Verhalten weiter funktioniert. Du lernst, wie daraus Vertrauen statt Zufall entsteht.
Testing Mindset bedeutet, dass du Tests nicht als lästige Zusatzarbeit siehst, sondern als Teil deiner Entwicklungsarbeit. Ein Test ist dabei ein ausführbarer Beleg: Dieses wichtige Verhalten hat gestern funktioniert, und es funktioniert nach deiner Änderung immer noch. Für Android-Apps ist diese Haltung besonders wertvoll, weil UI, Lebenszyklus, Netzwerk, Datenhaltung, Navigation und Gerätevielfalt schnell zu Fehlern führen können, die du manuell nicht zuverlässig jedes Mal findest.
Was ist das?
Ein Testing Mindset ist eine Denkweise, bei der du zuerst fragst: Welches Verhalten muss stabil bleiben, damit die App für Nutzerinnen und Nutzer zuverlässig ist? Es geht nicht darum, möglichst viele Tests zu schreiben oder jede Zeile Code zwanghaft abzudecken. Es geht darum, Vertrauen in die wichtigen Pfade deiner App aufzubauen: Login, Datenspeicherung, Offline-Zustand, Formularvalidierung, Navigation, Fehleranzeige oder Berechnungen.
Im Android-Kontext heißt das: Du testest nicht nur Methoden, sondern beobachtbares Verhalten. Wenn ein ViewModel bei einem erfolgreichen Repository-Aufruf einen UI-State mit Daten ausgibt, ist das Verhalten relevant. Ob intern eine private Hilfsmethode, ein bestimmter Mapper oder eine lokale Variable verwendet wird, ist dagegen meist weniger wichtig. Diese Unterscheidung schützt dich vor brüchigen Tests, die bei jeder kleinen Umstrukturierung kaputtgehen, obwohl die App aus Nutzersicht weiterhin korrekt arbeitet.
Für Lernende ist das mentale Modell wichtig: Tests sind kein Ersatz für Denken, Debugging oder Code-Review. Sie sind ein Sicherheitsnetz für bekannte Erwartungen. Du beschreibst eine Situation, führst Code aus und prüfst das Ergebnis. Dadurch werden Annahmen sichtbar. Wenn später jemand den Code ändert, zeigt der Test, ob eine Annahme verletzt wurde. Genau dadurch helfen Tests gegen Regressionen, also gegen Fehler, die nach einer Änderung in bereits funktionierende Bereiche zurückkehren.
In der Roadmap passt Testing Mindset zu Software Engineering Fundamentals, weil es dich vom reinen „Code läuft bei mir“ zu professionellerer Entwicklung führt. Seniorere Android-Entwicklung bedeutet nicht, jede API auswendig zu kennen. Es bedeutet auch, Änderungen so zu machen, dass andere sie prüfen, warten und sicher ausliefern können. Tests liefern dafür konkrete Evidenz.
Wie funktioniert es?
Das Testing Mindset beginnt vor dem Testframework. Du formulierst zuerst das gewünschte Verhalten in Alltagssprache. Zum Beispiel: „Wenn die E-Mail-Adresse leer ist, soll der Login-Button deaktiviert bleiben.“ Oder: „Wenn das Laden fehlschlägt, soll der UI-State eine Fehlermeldung enthalten.“ Erst danach entscheidest du, welche Testart passt.
In modernen Android-Projekten triffst du häufig drei Ebenen. Lokale Unit-Tests laufen auf der JVM und sind schnell. Sie eignen sich für Geschäftslogik, ViewModels, Mapper, Validatoren und Coroutine- oder Flow-Logik, wenn du Abhängigkeiten sauber kontrollieren kannst. Instrumentierte Tests laufen auf Gerät oder Emulator und prüfen Android-Komponenten mit echtem Framework-Verhalten. UI-Tests, zum Beispiel für Compose, prüfen, ob ein bestimmter Screen nutzbar reagiert. Jede Ebene hat Kosten und Nutzen. Ein Testing Mindset heißt, nicht blind die teuerste Ebene zu wählen, sondern dort zu testen, wo der Fehler mit vertretbarem Aufwand sichtbar wird.
Ein gutes Testdesign folgt oft dem Muster Arrange, Act, Assert. Du bereitest den Zustand vor, führst die relevante Aktion aus und prüfst das Ergebnis. Dieses Muster klingt schlicht, hilft aber gegen unklare Tests. Wenn ein Test mehrere verschiedene Dinge vorbereitet, mehrere Aktionen ausführt und am Ende zehn Erwartungen prüft, ist er schwer zu verstehen. Bei einem Fehler weißt du dann nicht sofort, welches Verhalten gebrochen ist.
Für Android ist Architektur ein wichtiger Helfer. Wenn deine Geschäftslogik komplett in einer Activity steckt, brauchst du für viele Prüfungen ein Gerät oder einen Emulator. Wenn du Logik in ViewModels, Use Cases oder Repository-Schnittstellen trennst, kannst du viele Fälle lokal und schnell testen. Jetpack Compose unterstützt diese Denkweise ebenfalls: UI sollte möglichst aus State entstehen. Dann kannst du Logik auf State-Ebene testen und zusätzlich wenige gezielte Compose-Tests für kritische Interaktionen schreiben.
Tests schaffen Vertrauen aber nur, wenn sie regelmäßig laufen. Ein Test, den du einmal schreibst und danach selten ausführst, hat wenig Wert. In echten Teams laufen Tests lokal vor größeren Änderungen und automatisiert in Pull Requests oder vor Releases. So werden Regressionen früh sichtbar. Qualität entsteht nicht durch einen einzelnen großen Testlauf kurz vor Veröffentlichung, sondern durch wiederholte, kleine Prüfungen während der Entwicklung.
Wichtig ist auch, was Tests nicht leisten. Sie beweisen nicht, dass eine App fehlerfrei ist. Sie zeigen nur, dass die geprüften Erwartungen unter den getesteten Bedingungen erfüllt sind. Deshalb musst du bewusst auswählen, welche Erwartungen wichtig sind. Kritische Nutzerpfade, Fehlerfälle und Bereiche mit häufiger Änderung verdienen mehr Aufmerksamkeit als rein dekorative Details.
In der Praxis
Stell dir vor, du baust einen Login-Screen mit Compose. Die UI zeigt Eingabefelder, aber die Entscheidung, ob der Login-Button aktiv ist, liegt im ViewModel. Ein Testing Mindset führt dich zu der Frage: Welches Verhalten muss stabil bleiben? Eine mögliche Antwort ist: Der Button darf nur aktiv sein, wenn E-Mail und Passwort gültig wirken. Das ist ein fachliches Verhalten, kein UI-Detail. Deshalb kann es lokal getestet werden.
Ein vereinfachtes ViewModel könnte so aussehen:
data class LoginUiState(
val email: String = "",
val password: String = "",
val canSubmit: Boolean = false
)
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginUiState())
val state: StateFlow<LoginUiState> = _state.asStateFlow()
fun onEmailChanged(value: String) {
update(email = value, password = _state.value.password)
}
fun onPasswordChanged(value: String) {
update(email = _state.value.email, password = value)
}
private fun update(email: String, password: String) {
_state.value = LoginUiState(
email = email,
password = password,
canSubmit = email.contains("@") && password.length >= 8
)
}
}
Ein passender lokaler Test prüft das sichtbare Verhalten des State:
class LoginViewModelTest {
@Test
fun `submit is enabled when email and password are valid`() {
val viewModel = LoginViewModel()
viewModel.onEmailChanged("dev@example.com")
viewModel.onPasswordChanged("12345678")
assertTrue(viewModel.state.value.canSubmit)
}
@Test
fun `submit stays disabled when password is too short`() {
val viewModel = LoginViewModel()
viewModel.onEmailChanged("dev@example.com")
viewModel.onPasswordChanged("123")
assertFalse(viewModel.state.value.canSubmit)
}
}
Der Test interessiert sich nicht dafür, ob die Prüfung in update, in einer privaten Funktion oder später in einem eigenen Validator liegt. Das ist Absicht. Du willst die Regel schützen, nicht die aktuelle innere Form des Codes. Wenn du später die Validierung auslagerst, sollten die Tests weiterhin beschreiben: Bei gültigen Eingaben kann gesendet werden, bei zu kurzem Passwort nicht.
Eine praktische Entscheidungsregel lautet: Teste zuerst das Verhalten, das bei einer Regression echten Schaden verursacht oder Nutzer sichtbar blockiert. Wenn ein falsch gesetzter Innenabstand im Layout auffällt, aber nicht kritisch ist, muss er nicht deine erste Testpriorität sein. Wenn ein Fehler dagegen Nutzer ausloggt, falsche Daten speichert oder einen Kaufprozess blockiert, verdient dieser Pfad automatisierte Prüfung.
Im Alltag kannst du so vorgehen: Wenn du einen Bug behebst, schreibe nach Möglichkeit zuerst einen Test, der den Bug reproduziert. Der Test sollte fehlschlagen, solange der Bug existiert. Danach korrigierst du den Code, bis der Test grün ist. So wird aus einem gefundenen Fehler eine dauerhafte Absicherung. Beim nächsten Refactoring zeigt dir der Test, ob der alte Fehler zurückgekommen ist.
Eine typische Stolperfalle ist das Testen von Implementierungsdetails. Beispiel: Du prüfst, dass eine bestimmte Repository-Methode exakt einmal aufgerufen wurde, obwohl das relevante Ergebnis eigentlich ein bestimmter UI-State ist. Solche Tests brechen schnell, wenn du Caching, Debouncing oder eine andere interne Struktur einführst. Besser ist oft: Stelle kontrollierte Eingabedaten bereit und prüfe, welcher State oder welches Ergebnis nach außen sichtbar wird. Mocking ist nützlich, aber zu viel Mocking kann Tests erzeugen, die nur deine aktuelle Verdrahtung bestätigen.
Eine zweite Stolperfalle ist der Glaube, manuelle Tests würden reichen. Natürlich klickst du neue Features selbst durch. Aber manuelles Prüfen ist vergesslich, langsam und abhängig von Aufmerksamkeit. Automatisierte Tests sind besonders stark bei Wiederholung. Sie prüfen dieselben Erwartungen nach jeder Änderung, auch an Stellen, an die du gerade nicht denkst.
Bei Compose solltest du ebenfalls nach Verhalten fragen. Ein UI-Test kann prüfen, ob ein Button bei ungültiger Eingabe deaktiviert ist oder ob nach einem Klick eine Fehlermeldung erscheint. Er sollte nicht jeden Textknoten ohne fachlichen Grund festnageln. Sonst werden kleine Textänderungen zu Testbrüchen, obwohl kein relevantes Verhalten beschädigt wurde. Für stabile Compose-Tests helfen klare Semantics, sinnvolle Test-Tags nur dort, wo sie nötig sind, und eine UI, die aus nachvollziehbarem State gerendert wird.
Auch Coroutines und Flow passen gut zu diesem Denken. Wenn ein ViewModel beim Start Daten lädt, kannst du mit Test-Dispatchern und kontrollierten Fake-Repositories prüfen, welche States nacheinander entstehen: Loading, Success oder Error. Entscheidend ist wieder nicht, wie viele interne Coroutines gestartet werden, sondern was die App nach außen meldet. Für Lernende ist das eine gute Übung: Schreibe Tests, die fachliche Zustände prüfen, bevor du dich in technische Details der Nebenläufigkeit verlierst.
In Code-Reviews kannst du dein Testing Mindset ebenfalls zeigen. Frage bei einer Änderung: Welches Verhalten könnte dadurch kaputtgehen? Gibt es bereits einen Test, der dieses Verhalten schützt? Falls nicht, wäre ein kleiner Test sinnvoller als ein langer Kommentar im Review. Gute Tests dokumentieren Erwartungen näher am Code und bleiben ausführbar.
Fazit
Testing Mindset heißt, dass du Tests als ausführbare Evidenz für wichtiges Verhalten behandelst: Sie reduzieren Regressionen und geben dir Vertrauen bei Änderungen, Refactorings und Releases. Prüfe beim nächsten Android-Feature bewusst einen kritischen Pfad: Formuliere das Verhalten in einem Satz, schreibe einen kleinen Test dafür, führe ihn aus und lies den Fehlerfall genau. Danach kannst du im Debugger oder im Code-Review prüfen, ob dein Test wirklich Nutzerverhalten schützt oder nur interne Details festschreibt.