WorkManager Testing: Worker gezielt prüfen
Teste Worker isoliert und prüfe Scheduling-Annahmen gezielt. So erkennst du Fehler bei Constraints, Retry und Coroutine-Code früher.
WorkManager Testing bedeutet, dass du Hintergrundarbeit nicht nur „irgendwie laufen lässt“, sondern das Verhalten deiner Worker und deine Scheduling-Annahmen kontrolliert prüfst. In echten Android-Apps ist das wichtig, weil Uploads, Syncs, Bereinigungen und Benachrichtigungen oft außerhalb des sichtbaren Compose-Screens passieren und trotzdem zuverlässig bleiben müssen.
Was ist das?
WorkManager ist die Jetpack-Komponente für verschiebbare, zuverlässige Hintergrundarbeit. Ein Worker beschreibt eine konkrete Aufgabe: zum Beispiel Entwürfe synchronisieren, alte Cache-Dateien löschen oder Messwerte hochladen. WorkManager entscheidet später, wann diese Aufgabe wirklich laufen darf. Dabei spielen Constraints eine zentrale Rolle, etwa Netzwerkverfügbarkeit, Ladezustand oder Speicherplatz.
WorkManager Testing ist der gezielte Test dieser Aufgaben und Annahmen. Du prüfst nicht, ob Androids System-Scheduler korrekt implementiert ist. Das ist Aufgabe der Plattform. Du prüfst, ob dein Worker bei bestimmten Eingaben das richtige Ergebnis liefert, ob er Fehler sinnvoll behandelt und ob deine WorkRequest-Konfiguration zu deinen fachlichen Erwartungen passt.
Das mentale Modell ist: Ein Worker besteht aus zwei Ebenen. Die erste Ebene ist deine fachliche Arbeit, also Repository aufrufen, Daten schreiben, Status melden und Ergebnis zurückgeben. Die zweite Ebene ist das Scheduling, also Constraints, Backoff, Initial Delay, eindeutige Arbeit und Verkettungen. Gute Tests trennen diese Ebenen so weit wie möglich. Dadurch findest du schneller heraus, ob ein Fehler in deiner Logik liegt oder in der Art, wie du Arbeit einplanst.
Im modernen Android-Kontext passt WorkManager Testing direkt zu Kotlin, Coroutines, Architektur und Qualitätssicherung. Viele Worker sind heute CoroutineWorker, weil Hintergrundarbeit oft suspendierende Repository-Funktionen, Datenbankzugriffe oder Netzwerkaufrufe nutzt. Wenn du dabei Dispatcher, Abhängigkeiten und Testdaten sauber injizierst, kannst du Worker in Isolation testen. Das ist besonders hilfreich, wenn dein UI in Compose nur anzeigt, dass ein Sync läuft oder abgeschlossen ist. Der Test des Compose-Screens ersetzt nicht den Test des Workers.
Wie funktioniert es?
WorkManager stellt für Tests eigene Hilfsmittel bereit. Das wichtigste Konzept ist der Test-Treiber, also TestDriver. Mit ihm steuerst du Bedingungen, die im echten System von außen kommen würden. Du kannst zum Beispiel festlegen, dass alle Constraints für eine bestimmte WorkRequest erfüllt sind. Du kannst auch eine Initial Delay als erfüllt markieren. Dadurch musst du im Test nicht warten und brauchst kein echtes WLAN, keinen echten Akku-Zustand und keine zufällige Systementscheidung.
Typisch ist eine Testumgebung mit WorkManagerTestInitHelper. Du initialisierst WorkManager für den Test mit einer speziellen Konfiguration. Danach planst du deine OneTimeWorkRequest oder PeriodicWorkRequest ein und nutzt den TestDriver, um Scheduling-Bedingungen kontrolliert auszulösen. Anschließend prüfst du den WorkInfo-Status oder die Nebenwirkungen, etwa ob ein Repository aufgerufen oder ein Datensatz geschrieben wurde.
Bei CoroutineWorker ist zusätzlich wichtig, dass du deterministisch testest. Suspendierende Funktionen dürfen im Test nicht von echtem Timing abhängen. In deiner Architektur sollten Worker daher möglichst wenig selbst bauen. Sie sollten Abhängigkeiten wie Repository, API-Client, DAO oder Clock über eine Factory oder Dependency Injection bekommen. So kannst du im Test Fakes einsetzen. Das folgt denselben Grundideen wie bei ViewModels: Nebenläufigkeit und I/O gehören kontrolliert, nicht zufällig in den Test.
Ein Worker liefert am Ende ein Result: success, failure oder retry. Diese drei Ergebnisse solltest du fachlich verstehen. success heißt: Die Aufgabe ist erledigt. failure heißt: Die Aufgabe ist fachlich oder dauerhaft gescheitert. retry heißt: Ein späterer Versuch ist sinnvoll. Ein häufiger Fehler ist, jeden Netzwerkfehler als failure zu behandeln. Für temporäre Probleme ist oft retry passend, während ungültige lokale Eingaben eher failure sind.
Constraints sind ebenfalls ein häufiger Prüfpunkt. Wenn du einen Upload nur bei Netzwerk erlaubst, testest du nicht, ob Android Netzwerk erkennt. Du testest, dass deine WorkRequest wirklich die passende Constraint enthält und dass der Worker erst nach freigegebenen Constraints läuft. Genau hier hilft der TestDriver. Ohne ihn werden Tests langsam, brüchig und abhängig von der Umgebung.
In der Praxis
Stell dir eine App vor, in der Nutzer offline Notizen schreiben. Ein SyncNotesWorker lädt lokale Änderungen hoch, sobald Netzwerk verfügbar ist. Die Compose-Oberfläche zeigt nur den lokalen Zustand und vielleicht einen Sync-Hinweis. Die eigentliche Zuverlässigkeit liegt aber im Worker. Deshalb testest du zwei Dinge: Der Worker ruft bei Erfolg das Repository auf und gibt success zurück. Außerdem ist die WorkRequest so konfiguriert, dass sie Netzwerk benötigt.
Ein vereinfachter Worker könnte so aussehen:
class SyncNotesWorker(
appContext: Context,
params: WorkerParameters,
private val repository: NotesRepository
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
return try {
repository.syncPendingNotes()
Result.success()
} catch (error: IOException) {
Result.retry()
} catch (error: IllegalStateException) {
Result.failure()
}
}
}
fun syncNotesRequest(): OneTimeWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
return OneTimeWorkRequestBuilder<SyncNotesWorker>()
.setConstraints(constraints)
.addTag("notes-sync")
.build()
}
Für einen isolierten Worker-Test würdest du ein Fake-Repository einsetzen. Der Test sollte nicht gegen deinen echten Server laufen und nicht von einer echten Room-Datenbank abhängen, wenn du nur die Worker-Entscheidung prüfen willst. Du kannst zum Beispiel prüfen, dass ein temporärer Netzwerkfehler zu retry führt:
class FakeNotesRepository(
private val error: Throwable? = null
) : NotesRepository {
var syncCalls = 0
override suspend fun syncPendingNotes() {
syncCalls++
error?.let { throw it }
}
}
@Test
fun syncWorker_returnsRetry_whenNetworkFails() = runTest {
val repository = FakeNotesRepository(error = IOException())
val worker = TestListenableWorkerBuilder<SyncNotesWorker>(context)
.setWorkerFactory(SyncWorkerFactory(repository))
.build()
val result = worker.doWork()
assertThat(result).isEqualTo(ListenableWorker.Result.retry())
assertThat(repository.syncCalls).isEqualTo(1)
}
Der genaue Aufbau der WorkerFactory hängt von deinem DI-Ansatz ab. In einer Hilt-App sieht das anders aus als in einer kleinen Beispiel-App. Die Regel bleibt gleich: Der Worker soll im Test seine Abhängigkeiten kontrolliert bekommen. Wenn du im Worker selbst Retrofit.Builder, Datenbanken oder globale Singletons erzeugst, machst du ihn schwer testbar und erhöhst das Risiko, dass Tests langsam und unzuverlässig werden.
Für Scheduling-Annahmen nutzt du eine WorkManager-Testumgebung. Der Ablauf ist: Test-WorkManager initialisieren, WorkRequest einreihen, TestDriver holen, Constraints freigeben, Ergebnis beobachten. Das sieht in vielen Projekten ungefähr so aus:
@Test
fun syncRequest_runsAfterConstraintsAreMet() {
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
val workManager = WorkManager.getInstance(context)
val request = syncNotesRequest()
workManager.enqueue(request).result.get()
val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
testDriver?.setAllConstraintsMet(request.id)
val workInfo = workManager.getWorkInfoById(request.id).get()
assertThat(workInfo.state).isIn(
listOf(WorkInfo.State.SUCCEEDED, WorkInfo.State.RUNNING)
)
}
In realen Tests würdest du den Zustand oft genauer beobachten, zum Beispiel mit WorkInfo oder einem Fake, der eine klare Nebenwirkung liefert. Der wichtige Punkt ist nicht die exakte Assertion aus diesem Kurzbeispiel, sondern die Kontrolle: Du wartest nicht passiv auf das System, sondern setzt die Bedingung, die dein Testfall braucht.
Eine typische Stolperfalle ist die Vermischung von Worker-Test und End-to-End-Test. Wenn ein Test Netzwerk, Datenbank, echte Zeitverzögerung und WorkManager-Scheduling gleichzeitig nutzt, ist er schwer zu verstehen. Scheitert er, weißt du nicht sofort, welcher Teil kaputt ist. Besser ist eine kleine Testpyramide: Die Worker-Logik testest du isoliert mit Fakes. Die WorkRequest-Konfiguration prüfst du separat. Nur wenige größere Tests prüfen das Zusammenspiel über mehrere Schichten.
Eine zweite Stolperfalle betrifft Coroutines und Flow. Wenn dein Worker einen Flow sammelt, muss klar sein, wann die Sammlung endet. Ein Worker, der endlos einen Flow beobachtet, blockiert seine Arbeit und erreicht kein Ergebnis. Für WorkManager brauchst du meist eine begrenzte Operation: einmal Daten lesen, einmal synchronisieren, Ergebnis zurückgeben. Nutze für solche Fälle eher first(), eine Repository-Funktion mit klarer Rückgabe oder eine begrenzte Sammlung. Endlose Beobachtung gehört eher in UI-nahe Schichten oder länger laufende Dienste, nicht in einen normalen Worker.
Eine praktische Entscheidungsregel lautet: Teste im Worker nicht „ob WorkManager funktioniert“, sondern „was meine App WorkManager zu tun gibt und wie mein Worker darauf reagiert“. Frage dich im Code-Review: Hat der Worker klare Abhängigkeiten? Sind success, failure und retry bewusst gewählt? Sind Constraints fachlich begründet? Gibt es Tests für mindestens einen erfolgreichen und einen fehlerhaften Pfad? Wenn du diese Fragen beantworten kannst, ist der Worker deutlich besser wartbar.
Fazit
WorkManager Testing hilft dir, Hintergrundarbeit verlässlich zu entwickeln, ohne dich auf Zufall, echte Wartezeiten oder wechselnde Gerätezustände zu verlassen. Übe das an einem kleinen CoroutineWorker: Schreibe zuerst einen Test für success, dann einen für retry, und prüfe anschließend mit dem TestDriver eine Constraint-Annahme. Danach lies deinen Worker im Code-Review mit genau dieser Frage: Ist die fachliche Arbeit isoliert prüfbar, und ist das Scheduling bewusst konfiguriert?