Android Coden
Android 4 min lesen

Architektur testen: Verhalten statt Implementierung prüfen

Gute Architekturtests prüfen öffentliches Verhalten, nicht interne Verdrahtung. So baust du stabile, wartbare Test-Suites für moderne Android-Apps.

Architekturtests sind der Unterschied zwischen einer Test-Suite, die nach jedem Refactoring bricht, und einer, die zuverlässig grün bleibt, solange deine App das Richtige tut. Wer stabile Android-Apps baut, testet nicht, wie eine Klasse intern aufgebaut ist, sondern was sie nach außen verspricht – und genau diese Unterscheidung ist der Kern von Architecture Testing.

Was ist das?

Architecture Testing bedeutet, die Grenzen zwischen den Schichten deiner Anwendung mit gezielten Tests abzusichern. Statt in ein ViewModel hineinzuschauen und seine privaten Felder zu lesen, stellst du Fragen wie: „Wenn ich diesen UI-Event sende, welchen State liefert der StateFlow zurück?” Du testest Verhalten (behavior), das durch öffentliche Schnittstellen sichtbar ist – nicht die private Verdrahtung (wiring) dahinter.

In einer modernen Android-App nach dem offiziellen Architecture Guide bilden UI-Layer, Domain-Layer und Data-Layer klare Grenzen (boundaries). Architecture Testing respektiert genau diese Grenzen: Ein Test für das ViewModel kommuniziert ausschließlich über dessen öffentliche API. Ein Test für einen Use-Case ruft nur dessen öffentliche Funktion auf. Was hinter der Grenze passiert, bleibt dem Test verborgen – und genau das macht ihn robust gegen interne Umbauten.

Das klingt wie eine Stilfrage, ist aber eine Stabilitätsfrage. Tests, die Implementierungsdetails kennen, müssen bei jedem Refactoring aktualisiert werden, selbst wenn das beobachtbare Verhalten identisch bleibt. Tests, die nur Grenzen kennen, überleben Refactorings ohne Änderung.

Wie funktioniert es?

Der zentrale Mechanismus ist die Entkopplung über Schnittstellen und Fakes. Eine Fake-Implementierung ahmt das Verhalten einer echten Klasse nach, ohne deren Infrastruktur zu benötigen. Ein FakeUserRepository implementiert UserRepository, liefert aber vordefinierte Testdaten zurück, statt eine Datenbank oder ein Netzwerk anzufragen.

class FakeUserRepository : UserRepository {
    private val users = mutableListOf<User>()

    override suspend fun getUser(id: String): User? =
        users.find { it.id == id }

    fun addUser(user: User) {
        users.add(user)
    }
}

Dieses Fake wird dem ViewModel über den Konstruktor übergeben – kein Mocking-Framework, kein Reflection, keine versteckten Abhängigkeiten. Das ViewModel erfährt gar nicht, dass es gegen ein Fake läuft, weil es nur die Schnittstelle kennt.

Ein typischer Test folgt dem Arrange-Act-Assert-Muster und arbeitet ausschließlich mit dem öffentlichen Zustand des ViewModels:

@Test
fun `when user exists, ui state shows user name`() = runTest {
    // Arrange
    val repo = FakeUserRepository()
    repo.addUser(User(id = "42", name = "Ada"))
    val viewModel = UserViewModel(repo)

    // Act
    viewModel.loadUser("42")

    // Assert
    val state = viewModel.uiState.value
    assertIs<UserUiState.Success>(state)
    assertEquals("Ada", state.user.name)
}

Der Test prüft uiState – die öffentliche Schnittstelle des ViewModels. Ob das ViewModel intern einen Cache nutzt, eine Coroutine startet oder einen direkten Aufruf macht, ist für diesen Test vollständig irrelevant.

In der Praxis

Grenzen vor dem Schreiben identifizieren. Bevor du einen Test aufsetzt, benenne die Grenze: Was ist die öffentliche Schnittstelle dieser Klasse? Für ein ViewModel sind das State-Flows und empfangene Events. Für ein Repository sind das suspend-Funktionen und zurückgegebene Flows. Schreibe ausschließlich Tests, die diese Schnittstelle verwenden – nie tiefer.

Fakes bevorzugen, Mocks vermeiden. Mocking-Frameworks wie Mockito verleiten dazu, Implementierungsdetails zu stubben. Ein whenever(repo.getUser(any())).thenReturn(...) bindet den Test an den konkreten Methodenaufruf – nicht an das Verhalten. Fakes halten sich dagegen an dieselbe Abstraktion wie die Produktionsimplementierung: Schnittstelle rein, Verhalten raus. Sie sind leichter zu lesen, leichter zu debuggen und überleben API-Änderungen im Repository besser.

Typische Stolperfalle: Interner Zustand als Assertion. Viele Einsteiger greifen über Reflection auf private Felder zu oder prüfen, ob eine bestimmte Methode aufgerufen wurde. Das testet Wiring, kein Verhalten. Sobald du das ViewModel intern umstrukturierst – ohne das sichtbare Verhalten zu ändern – werden diese Tests rot. Das ist das deutlichste Signal dafür, dass du die falsche Ebene testest.

Testpyramide beachten. Architekturtests mit Fakes fallen meist in die schnelle Unit-Test-Schicht und benötigen kein Android-Framework. Integrationstests – etwa mit einer Room-In-Memory-Datenbank statt einem Fake-Repository – prüfen die Grenze zwischen Domain-Layer und Data-Layer unter realistischeren Bedingungen. End-to-End-Tests mit Compose UI Test oder Espresso sind wertvoller für vollständige User-Flows, aber deutlich langsamer. Setze sie gezielt ein und halte die Unit-Test-Schicht groß.

Fazit

Architecture Testing ist keine Frage der Werkzeuge, sondern eine Frage der Disziplin: Du hörst auf, Implementierungen zu testen, und fängst an, Verhalten zu testen. Öffne heute eine deiner bestehenden Test-Klassen und prüfe, wie viele Assertions direkt gegen Implementierungsdetails schreiben – jede davon ist ein Kandidat für eine Umschreibung. Baue dann ein einfaches Fake für dein wichtigstes Repository, tausche es in einem ViewModel-Test ein und beobachte, wie das Refactoring plötzlich möglich wird, ohne einen einzigen Test anpassen zu müssen.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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