Dieses Codelab ist Teil des Kurses „Advanced Android in Kotlin“. Sie können den größten Nutzen aus diesem Kurs ziehen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Das ist jedoch nicht zwingend erforderlich. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu „Android für Fortgeschrittene mit Kotlin“ aufgeführt.
Einführung
In diesem zweiten Codelab zum Testen geht es um Test-Doubles: wann sie in Android verwendet werden sollten und wie sie mit Dependency Injection, dem Service Locator-Muster und Bibliotheken implementiert werden. Dabei lernen Sie, wie Sie Folgendes schreiben:
- Repository-Einheitentests
- Integrationstests für Fragmente und ViewModels
- Fragmentnavigationstests
Was Sie bereits wissen sollten
Sie sollten mit Folgendem vertraut sein:
- Die Programmiersprache Kotlin
- Testkonzepte, die im ersten Codelab behandelt werden: Einheitentests in Android mit JUnit, Hamcrest, AndroidX Test und Robolectric schreiben und ausführen sowie LiveData testen
- Die folgenden Android Jetpack-Kernbibliotheken:
ViewModel,LiveDataund die Navigationskomponente - Anwendungsarchitektur gemäß dem Muster aus dem Leitfaden zur App-Architektur und den Android-Grundlagen-Codelabs
- Grundlagen von Coroutinen unter Android
Lerninhalte
- Teststrategie planen
- Test-Doubles erstellen und verwenden, insbesondere Fakes und Mocks
- Manuelle Abhängigkeitsinjektion in Android für Unit- und Integrationstests verwenden
- Service Locator-Muster anwenden
- Repositorys, Fragmente, Ansichtsmodelle und die Navigationskomponente testen
Sie verwenden die folgenden Bibliotheken und Codekonzepte:
Aufgaben
- Unittests für ein Repository mit einem Test-Double und Abhängigkeitsinjektion schreiben
- Unittests für ein View-Modell mit einem Test-Double und Dependency Injection schreiben
- Integrationstests für Fragmente und ihre ViewModels mit dem Espresso-UI-Test-Framework schreiben
- Navigationstests mit Mockito und Espresso schreiben
In dieser Reihe von Codelabs arbeiten Sie mit der App „TO-DO Notes“. Mit der App können Sie Aufgaben aufschreiben, die Sie erledigen müssen, und sie werden in einer Liste angezeigt. Sie können sie dann als erledigt oder nicht erledigt markieren, filtern oder löschen.

Diese App ist in Kotlin geschrieben, hat einige Bildschirme, verwendet Jetpack-Komponenten und folgt der Architektur aus der Anleitung zur App-Architektur. Wenn Sie lernen, wie Sie diese App testen, können Sie auch Apps testen, die dieselben Bibliotheken und dieselbe Architektur verwenden.
Code herunterladen
Laden Sie zuerst den Code herunter:
Alternativ können Sie das Github-Repository für den Code klonen:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
Machen Sie sich anhand der Anleitung unten mit dem Code vertraut.
Schritt 1: Beispiel-App ausführen
Öffnen Sie die TO-DO-App in Android Studio und führen Sie sie aus. Es sollte kompiliert werden. So können Sie die App ausprobieren:
- Erstellen Sie eine neue Aufgabe mit der schwebenden Aktionsschaltfläche „Pluszeichen“. Geben Sie zuerst einen Titel und dann zusätzliche Informationen zur Aufgabe ein. Speichern Sie sie mit der grünen schwebenden Aktionsschaltfläche.
- Klicken Sie in der Aufgabenliste auf den Titel der gerade erledigten Aufgabe und sehen Sie sich auf dem Detailbildschirm für diese Aufgabe den Rest der Beschreibung an.
- Setzen Sie in der Liste oder auf dem Detailbildschirm ein Häkchen in das Kästchen der Aufgabe, um den Status auf Abgeschlossen zu setzen.
- Kehren Sie zum Aufgabenbildschirm zurück, öffnen Sie das Filtermenü und filtern Sie die Aufgaben nach dem Status Aktiv und Abgeschlossen.
- Öffne die Navigationsleiste und klicke auf Statistiken.
- Kehren Sie zum Übersichtsbildschirm zurück und wählen Sie im Navigationsmenü Abgeschlossene löschen aus, um alle Aufgaben mit dem Status Abgeschlossen zu löschen.
Schritt 2: Beispiel-App-Code ansehen
Die TO-DO-App basiert auf dem beliebten Test- und Architekturbeispiel Architecture Blueprints (mit der reaktiven Architektur-Version des Beispiels). Die App folgt der Architektur aus dem Leitfaden zur App-Architektur. Es verwendet ViewModels mit Fragments, ein Repository und Room. Wenn Sie mit einem der folgenden Beispiele vertraut sind, hat diese App eine ähnliche Architektur:
- Codelab „Room with a View“
- Codelabs für die Android Kotlin Fundamentals-Schulung
- Codelabs für Fortgeschrittene für Android
- Android Sunflower-Beispiel
- Udacity-Schulungskurs „Android-Apps mit Kotlin entwickeln“
Es ist wichtiger, dass Sie die allgemeine Architektur der App verstehen, als dass Sie die Logik auf einer bestimmten Ebene genau kennen.
Hier ist eine Zusammenfassung der Pakete:
Paket : | |
| Bildschirm zum Hinzufügen oder Bearbeiten einer Aufgabe:UI-Ebene-Code zum Hinzufügen oder Bearbeiten einer Aufgabe. |
| Die Datenschicht:Hier geht es um die Datenschicht der Aufgaben. Sie enthält den Datenbank-, Netzwerk- und Repository-Code. |
| Statistikbildschirm:UI-Layer-Code für den Statistikbildschirm. |
| Aufgabendetailansicht:UI-Ebene-Code für eine einzelne Aufgabe. |
| Aufgabenbildschirm:UI-Layer-Code für die Liste aller Aufgaben. |
| Hilfsklassen:Gemeinsam genutzte Klassen, die in verschiedenen Teilen der App verwendet werden, z.B. für das Layout zum Aktualisieren per Wischen, das auf mehreren Bildschirmen verwendet wird. |
Datenschicht (.data)
Diese App enthält eine simulierte Netzwerkschicht im Paket remote und eine Datenschicht im Paket local. Der Einfachheit halber wird die Netzwerkschicht in diesem Projekt nur mit einem HashMap mit einer Verzögerung simuliert, anstatt echte Netzwerkanfragen zu stellen.
Die DefaultTasksRepository-Koordinaten oder ‑Vermittlungen zwischen der Netzwerk- und der Datenbankebene und geben Daten an die UI-Ebene zurück.
UI-Ebene ( .addedittask, .statistics, .taskdetail, .tasks)
Jedes der UI-Layer-Pakete enthält ein Fragment und ein View-Modell sowie alle anderen Klassen, die für die Benutzeroberfläche erforderlich sind, z. B. einen Adapter für die Aufgabenliste. TaskActivity ist die Aktivität, die alle Fragmente enthält.
Navigation
Die Navigation für die App wird über die Navigationskomponente gesteuert. Sie ist in der Datei nav_graph.xml definiert. Die Navigation wird in den Ansichtsmodellen mit der Klasse Event ausgelöst. Die Ansichtsmodelle legen auch fest, welche Argumente übergeben werden sollen. Die Fragmente beobachten die Events und führen die tatsächliche Navigation zwischen den Bildschirmen durch.
In diesem Codelab erfahren Sie, wie Sie Repositories, Modelle und Fragmente mithilfe von Test Doubles und Dependency Injection testen. Bevor Sie sich ansehen, was das ist, sollten Sie sich mit den Gründen vertraut machen, die bestimmen, was und wie Sie diese Tests schreiben.
In diesem Abschnitt werden einige allgemeine Best Practices für Tests beschrieben, die für Android gelten.
Die Testpyramide
Bei der Entwicklung einer Teststrategie sind drei zusammenhängende Aspekte zu berücksichtigen:
- Umfang: Wie viel Code wird durch den Test abgedeckt? Tests können für eine einzelne Methode, für die gesamte Anwendung oder für einen Teil davon ausgeführt werden.
- Geschwindigkeit: Wie schnell wird der Test ausgeführt? Die Testgeschwindigkeit kann zwischen Millisekunden und mehreren Minuten variieren.
- Realitätsnähe: Wie realitätsnah ist der Test? Wenn beispielsweise ein Teil des zu testenden Codes eine Netzwerkanfrage stellen muss, wird diese Anfrage dann tatsächlich vom Testcode gestellt oder wird das Ergebnis gefälscht? Wenn der Test tatsächlich mit dem Netzwerk kommuniziert, ist er zuverlässiger. Der Nachteil ist, dass der Test länger dauern kann, zu Fehlern führen kann, wenn das Netzwerk ausfällt, oder teuer sein kann.
Zwischen diesen Aspekten bestehen inhärente Kompromisse. Beispiel: Geschwindigkeit und Genauigkeit sind ein Kompromiss. Je schneller der Test, desto geringer ist in der Regel die Genauigkeit und umgekehrt. Eine gängige Methode zur Unterteilung automatisierter Tests ist die Einteilung in die folgenden drei Kategorien:
- Einheitentests: Diese Tests sind sehr fokussiert und werden für eine einzelne Klasse ausgeführt, in der Regel für eine einzelne Methode in dieser Klasse. Wenn ein Unit-Test fehlschlägt, wissen Sie genau, wo in Ihrem Code das Problem liegt. Sie haben eine geringe Genauigkeit, da Ihre App in der Praxis viel mehr als die Ausführung einer Methode oder Klasse umfasst. Sie sind schnell genug, um jedes Mal ausgeführt zu werden, wenn Sie Ihren Code ändern. In den meisten Fällen handelt es sich um lokal ausgeführte Tests (im
test-Quellset). Beispiel : Einzelne Methoden in Viewmodels und Repositories testen. - Integrationstests: Bei diesen Tests wird die Interaktion mehrerer Klassen getestet, um sicherzustellen, dass sie sich bei gemeinsamer Verwendung wie erwartet verhalten. Eine Möglichkeit, Integrationstests zu strukturieren, besteht darin, dass sie eine einzelne Funktion testen, z. B. die Möglichkeit, eine Aufgabe zu speichern. Sie testen einen größeren Codebereich als Unit-Tests, sind aber immer noch auf Geschwindigkeit und nicht auf vollständige Genauigkeit optimiert. Sie können je nach Situation entweder lokal oder als Instrumentationstests ausgeführt werden. Beispiel : Alle Funktionen eines einzelnen Fragment- und Viewmodel-Paars testen.
- End-to-End-Tests (E2E): Testen Sie eine Kombination von Funktionen, die zusammenarbeiten. Sie testen große Teile der App, simulieren die tatsächliche Nutzung genau und sind daher in der Regel langsam. Sie haben die höchste Genauigkeit und zeigen Ihnen, dass Ihre Anwendung als Ganzes funktioniert. In der Regel handelt es sich dabei um instrumentierte Tests (im Quellset
androidTest).
Beispiel : Die gesamte App wird gestartet und einige Funktionen werden gemeinsam getestet.
Das empfohlene Verhältnis dieser Tests wird oft durch eine Pyramide dargestellt, wobei der Großteil der Tests Einheitentests sind.

Architektur und Tests
Ob Sie Ihre App auf allen Ebenen der Testpyramide testen können, hängt von der Architektur Ihrer App ab. Beispielsweise könnte eine extrem schlecht konzipierte Anwendung ihre gesamte Logik in einer Methode unterbringen. Sie können dafür möglicherweise einen End-to-End-Test schreiben, da diese Tests in der Regel große Teile der App abdecken. Aber wie sieht es mit Unit- oder Integrationstests aus? Da sich der gesamte Code an einem Ort befindet, ist es schwierig, nur den Code zu testen, der sich auf eine einzelne Einheit oder Funktion bezieht.
Ein besserer Ansatz wäre, die Anwendungslogik in mehrere Methoden und Klassen aufzuteilen, sodass jeder Teil isoliert getestet werden kann. Die Architektur ist eine Möglichkeit, Ihren Code aufzuteilen und zu organisieren, was Einheitentests und Integrationstests erleichtert. Die TO-DO-App, die Sie testen, folgt einer bestimmten Architektur:
In dieser Lektion erfahren Sie, wie Sie Teile der oben genannten Architektur isoliert testen:
- Zuerst führen Sie einen Unittest für das Repository aus.
- Anschließend verwenden Sie ein Test-Double im ViewModel, das für Unittests und Integrationstests des ViewModels erforderlich ist.
- Als Nächstes erfahren Sie, wie Sie Integrationstests für Fragmente und ihre ViewModels schreiben.
- Schließlich lernen Sie, Integrationstests zu schreiben, die die Navigation Component enthalten.
End-to-End-Tests werden in der nächsten Lektion behandelt.
Wenn Sie einen Einheitentest für einen Teil einer Klasse (eine Methode oder eine kleine Sammlung von Methoden) schreiben, sollten Sie nur den Code in dieser Klasse testen.
Es kann schwierig sein, nur Code in einer bestimmten Klasse oder in bestimmten Klassen zu testen. Sehen wir uns ein Beispiel an. Öffnen Sie die Klasse data.source.DefaultTaskRepository im Quellset main. Dies ist das Repository für die App und die Klasse, für die Sie als Nächstes Einheitentests schreiben.
Ihr Ziel ist es, nur den Code in dieser Klasse zu testen. DefaultTaskRepository ist jedoch von anderen Klassen wie LocalTaskDataSource und RemoteTaskDataSource abhängig. Anders ausgedrückt: LocalTaskDataSource und RemoteTaskDataSource sind Abhängigkeiten von DefaultTaskRepository.
Jede Methode in DefaultTaskRepository ruft also Methoden für Datenquellenklassen auf, die wiederum Methoden in anderen Klassen aufrufen, um Informationen in einer Datenbank zu speichern oder mit dem Netzwerk zu kommunizieren.

Sehen Sie sich beispielsweise diese Methode in DefaultTasksRepo an.
suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
if (forceUpdate) {
try {
updateTasksFromRemoteDataSource()
} catch (ex: Exception) {
return Result.Error(ex)
}
}
return tasksLocalDataSource.getTasks()
}getTasks ist einer der „einfachsten“ Aufrufe, die Sie an Ihr Repository senden können. Diese Methode umfasst das Lesen aus einer SQLite-Datenbank und das Ausführen von Netzwerkaufrufen (der Aufruf von updateTasksFromRemoteDataSource). Dies erfordert viel mehr Code als nur den Repository-Code.
Hier sind einige konkretere Gründe, warum das Testen des Repository schwierig ist:
- Sie müssen sich Gedanken über das Erstellen und Verwalten einer Datenbank machen, um selbst die einfachsten Tests für dieses Repository durchzuführen. Daraus ergeben sich Fragen wie „Sollte dies ein lokaler oder instrumentierter Test sein?“ und „Sollte ich AndroidX Test verwenden, um eine simulierte Android-Umgebung zu erhalten?“.
- Einige Teile des Codes, z. B. Netzwerkcode, können lange dauern oder gelegentlich sogar fehlschlagen, was zu lang andauernden, unzuverlässigen Tests führt.
- Ihre Tests verlieren möglicherweise die Fähigkeit, zu diagnostizieren, welcher Code für einen Testfehler verantwortlich ist. Ihre Tests könnten mit dem Testen von Code beginnen, der nicht zum Repository gehört. So könnten beispielsweise Ihre vermeintlichen Unit-Tests für das Repository aufgrund eines Problems in einem der abhängigen Codes, z. B. dem Datenbankcode, fehlschlagen.
Test-Doubles
Die Lösung besteht darin, dass Sie beim Testen des Repositorys nicht den echten Netzwerk- oder Datenbankcode verwenden, sondern stattdessen ein Test-Double. Ein Test-Double ist eine Version einer Klasse, die speziell für Tests entwickelt wurde. Sie soll die reale Version einer Klasse in Tests ersetzen. Das ist ähnlich wie bei einem Stuntdouble, das ein Schauspieler ist, der sich auf Stunts spezialisiert hat und den eigentlichen Schauspieler bei gefährlichen Aktionen ersetzt.
Hier sind einige Arten von Test-Doubles:
Gefälscht | Ein Test-Double mit einer „funktionierenden“ Implementierung der Klasse, die jedoch so implementiert ist, dass sie sich gut für Tests, aber nicht für die Produktion eignet. |
Mock | Ein Test-Double, das aufzeichnet, welche seiner Methoden aufgerufen wurden. Anschließend wird ein Test bestanden oder nicht bestanden, je nachdem, ob die Methoden richtig aufgerufen wurden. |
Stub | Ein Test-Double, das keine Logik enthält und nur das zurückgibt, was Sie ihm vorgeben. Ein |
Dummy | Ein Test-Double, das übergeben, aber nicht verwendet wird, z. B. wenn Sie es nur als Parameter angeben müssen. Wenn Sie ein |
Spy | Ein Test-Double, das auch einige zusätzliche Informationen erfasst, z. B. wie oft die Methode |
Weitere Informationen zu Test-Doubles finden Sie unter Testing on the Toilet: Know Your Test Doubles.
Die am häufigsten in Android verwendeten Test-Doubles sind Fakes und Mocks.
In dieser Aufgabe erstellen Sie ein FakeDataSource-Test-Double, um DefaultTasksRepository unabhängig von den tatsächlichen Datenquellen zu testen.
Schritt 1: Klasse „FakeDataSource“ erstellen
In diesem Schritt erstellen Sie eine Klasse mit dem Namen FakeDataSouce, die ein Test-Double von LocalDataSource und RemoteDataSource ist.
- Klicken Sie im Quellset test mit der rechten Maustaste und wählen Sie Neu -> Paket aus.

- Erstellen Sie ein data-Paket mit einem source-Paket darin.
- Erstellen Sie im Paket data/source eine neue Klasse mit dem Namen
FakeDataSource.

Schritt 2: TasksDataSource-Schnittstelle implementieren
Damit Sie Ihre neue Klasse FakeDataSource als Test-Double verwenden können, muss sie die anderen Datenquellen ersetzen können. Diese Datenquellen sind TasksLocalDataSource und TasksRemoteDataSource.

- Beachten Sie, dass beide die
TasksDataSource-Schnittstelle implementieren.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }- Lassen Sie
FakeDataSourceTasksDataSourceimplementieren:
class FakeDataSource : TasksDataSource {
}Android Studio meldet, dass Sie die erforderlichen Methoden für TasksDataSource nicht implementiert haben.
- Wählen Sie im Menü für schnelle Korrekturen Mitglieder implementieren aus.

- Wählen Sie alle Methoden aus und drücken Sie OK.

Schritt 3: Methode „getTasks“ in „FakeDataSource“ implementieren
FakeDataSource ist ein bestimmter Typ von Test-Double, der als Fake bezeichnet wird. Ein Fake ist ein Test-Double, das eine „funktionierende“ Implementierung der Klasse hat. Diese Implementierung ist jedoch so gestaltet, dass sie sich gut für Tests, aber nicht für die Produktion eignet. Eine „funktionierende“ Implementierung bedeutet, dass die Klasse bei bestimmten Eingaben realistische Ausgaben erzeugt.
Ihre gefälschte Datenquelle stellt beispielsweise keine Verbindung zum Netzwerk her und speichert nichts in einer Datenbank. Stattdessen wird nur eine In-Memory-Liste verwendet. Das funktioniert wie erwartet, da Methoden zum Abrufen oder Speichern von Aufgaben die erwarteten Ergebnisse zurückgeben. Sie können diese Implementierung jedoch niemals in der Produktion verwenden, da sie nicht auf dem Server oder in einer Datenbank gespeichert wird.
Ein FakeDataSource
- Damit können Sie den Code in
DefaultTasksRepositorytesten, ohne auf eine echte Datenbank oder ein echtes Netzwerk angewiesen zu sein. - bietet eine „realistische“ Implementierung für Tests.
- Ändern Sie den
FakeDataSource-Konstruktor, um einvarnamenstaskszu erstellen, das einMutableList<Task>?mit dem Standardwert einer leeren veränderlichen Liste ist.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Dies ist die Liste der Aufgaben, die eine Datenbank- oder Serverantwort „vortäuschen“. Das Ziel ist es, die getTasks-Methode des Repositorys zu testen. Dadurch werden die Methoden getTasks, deleteAllTasks und saveTask der Datenquelle aufgerufen.
Schreibe eine gefälschte Version dieser Methoden:
- Schreiben Sie
getTasks: Wenntasksnichtnullist, geben Sie einSuccess-Ergebnis zurück. Wenntasksnullist, wird einError-Ergebnis zurückgegeben. - Schreiben Sie
deleteAllTasks, um die Liste der veränderlichen Aufgaben zu löschen. - Schreiben Sie
saveTask, um die Aufgabe der Liste hinzuzufügen.
Diese Methoden, die für FakeDataSource implementiert wurden, sehen so aus:
override suspend fun getTasks(): Result<List<Task>> {
tasks?.let { return Success(ArrayList(it)) }
return Error(
Exception("Tasks not found")
)
}
override suspend fun deleteAllTasks() {
tasks?.clear()
}
override suspend fun saveTask(task: Task) {
tasks?.add(task)
}Hier sind die Importanweisungen, falls Sie sie benötigen:
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.TaskDas funktioniert ähnlich wie bei den tatsächlichen lokalen und Remote-Datenquellen.
In diesem Schritt verwenden Sie eine Technik namens manuelle Abhängigkeitsinjektion, damit Sie das gerade erstellte Test-Double verwenden können.
Das Hauptproblem ist, dass Sie eine FakeDataSource haben, aber nicht klar ist, wie Sie sie in den Tests verwenden. Er muss TasksRemoteDataSource und TasksLocalDataSource ersetzen, aber nur in den Tests. Sowohl TasksRemoteDataSource als auch TasksLocalDataSource sind Abhängigkeiten von DefaultTasksRepository. Das bedeutet, dass DefaultTasksRepositories diese Klassen für die Ausführung benötigt oder von ihnen „abhängt“.
Derzeit werden die Abhängigkeiten in der Methode init von DefaultTasksRepository erstellt.
DefaultTasksRepository.kt
class DefaultTasksRepository private constructor(application: Application) {
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
// Some other code
init {
val database = Room.databaseBuilder(application.applicationContext,
ToDoDatabase::class.java, "Tasks.db")
.build()
tasksRemoteDataSource = TasksRemoteDataSource
tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
}
// Rest of class
}Da Sie taskLocalDataSource und tasksRemoteDataSource in DefaultTasksRepository erstellen und zuweisen, sind sie im Grunde fest codiert. Es gibt keine Möglichkeit, das Test-Double zu verwenden.
Stattdessen sollten Sie diese Datenquellen der Klasse zur Verfügung stellen, anstatt sie hart zu codieren. Das Bereitstellen von Abhängigkeiten wird als Abhängigkeitsinjektion bezeichnet. Es gibt verschiedene Möglichkeiten, Abhängigkeiten bereitzustellen, und daher auch verschiedene Arten der Abhängigkeitsinjektion.
Mit der Constructor Dependency Injection (Konstruktor-Dependency Injection) können Sie das Test-Double einfügen, indem Sie es an den Konstruktor übergeben.
Kein Einschleusen
| Einschleusung
|
Schritt 1: Constructor Dependency Injection in DefaultTasksRepository verwenden
- Ändern Sie den Konstruktor von
DefaultTaskRepositoryso, dass er anstelle vonApplicationsowohl Datenquellen als auch den Coroutine-Dispatcher akzeptiert. Letzteren müssen Sie auch für Ihre Tests austauschen. Das wird im dritten Lektionsabschnitt zu Coroutines genauer beschrieben.
DefaultTasksRepository.kt
// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }
// WITH
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }- Da Sie die Abhängigkeiten übergeben haben, entfernen Sie die
init-Methode. Sie müssen die Abhängigkeiten nicht mehr erstellen. - Löschen Sie auch die alten Instanzvariablen. Sie definieren sie im Konstruktor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO- Aktualisieren Sie schließlich die Methode
getRepository, damit der neue Konstruktor verwendet wird:
DefaultTasksRepository.kt
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}Sie verwenden jetzt die Konstruktor-Abhängigkeitsinjektion.
Schritt 2: FakeDataSource in Tests verwenden
Da Ihr Code jetzt die Constructor Dependency Injection verwendet, können Sie Ihre gefälschte Datenquelle zum Testen von DefaultTasksRepository verwenden.
- Klicken Sie mit der rechten Maustaste auf den Klassennamen
DefaultTasksRepositoryund wählen Sie Generate (Generieren) und dann Test (Test) aus. - Folgen Sie der Anleitung, um
DefaultTasksRepositoryTestim Quellsatz test zu erstellen. - Fügen Sie oben in Ihrer neuen
DefaultTasksRepositoryTest-Klasse die folgenden Mitgliedsvariablen hinzu, um die Daten in Ihren gefälschten Datenquellen darzustellen.
DefaultTasksRepositoryTest.kt
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }- Erstellen Sie drei Variablen: zwei
FakeDataSource-Mitgliedsvariablen (eine für jede Datenquelle für Ihr Repository) und eine Variable für dieDefaultTasksRepository, die Sie testen möchten.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepositoryErstellen Sie eine Methode zum Einrichten und Initialisieren eines testbaren DefaultTasksRepository. Für diesen DefaultTasksRepository wird Ihr Test-Double FakeDataSource verwendet.
- Erstellen Sie eine Methode namens
createRepositoryund annotieren Sie sie mit@Before. - Instanziieren Sie Ihre gefälschten Datenquellen mit den Listen
remoteTasksundlocalTasks. - Instanziieren Sie
tasksRepositorymit den beiden gefälschten Datenquellen, die Sie gerade erstellt haben, undDispatchers.Unconfined.
Die endgültige Methode sollte wie im Code unten aussehen.
DefaultTasksRepositoryTest.kt
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}Schritt 3: Test für DefaultTasksRepository.getTasks() schreiben
Zeit, einen DefaultTasksRepository-Test zu schreiben.
- Schreiben Sie einen Test für die Methode
getTasksdes Repositorys. Prüfen Sie, ob beim Aufrufen vongetTasksmittrue(d. h. beim Neuladen aus der Remote-Datenquelle) Daten aus der Remote-Datenquelle (und nicht aus der lokalen Datenquelle) zurückgegeben werden.
DefaultTasksRepositoryTest.kt
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource(){
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}Beim Aufrufen von getTasks: wird ein Fehler ausgegeben.
Schritt 4: runBlockingTest hinzufügen
Der Coroutine-Fehler ist zu erwarten, da getTasks eine suspend-Funktion ist und Sie eine Coroutine starten müssen, um sie aufzurufen. Dazu benötigen Sie einen Coroutine-Scope. Um diesen Fehler zu beheben, müssen Sie einige Gradle-Abhängigkeiten für das Starten von Coroutinen in Ihren Tests hinzufügen.
- Fügen Sie dem Test-Quellset mit
testImplementationdie erforderlichen Abhängigkeiten für das Testen von Coroutinen hinzu.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"Vergiss nicht, deine Daten zu synchronisieren!
kotlinx-coroutines-test ist die Coroutines-Testbibliothek, die speziell für das Testen von Coroutines entwickelt wurde. Verwenden Sie die Funktion runBlockingTest, um Ihre Tests auszuführen. Dies ist eine Funktion, die von der Coroutines-Testbibliothek bereitgestellt wird. Es wird ein Codeblock als Eingabe verwendet und dieser Codeblock wird dann in einem speziellen Coroutinenkontext ausgeführt, der synchron und sofort ausgeführt wird. Das bedeutet, dass Aktionen in einer deterministischen Reihenfolge ausgeführt werden. Dadurch werden Ihre Coroutinen im Grunde wie Nicht-Coroutinen ausgeführt. Diese Funktion ist also für das Testen von Code vorgesehen.
Verwenden Sie runBlockingTest in Ihren Testklassen, wenn Sie eine suspend-Funktion aufrufen. Im nächsten Codelab dieser Reihe erfahren Sie mehr darüber, wie runBlockingTest funktioniert und wie Sie Coroutinen testen können.
- Fügen Sie
@ExperimentalCoroutinesApiüber der Klasse hinzu. Damit wird ausgedrückt, dass Sie wissen, dass Sie in der Klasse eine experimentelle Coroutine-API (runBlockingTest) verwenden. Andernfalls erhalten Sie eine Warnung. - Fügen Sie in Ihrem
DefaultTasksRepositoryTestrunBlockingTesthinzu, damit der gesamte Test als Codeblock betrachtet wird.
Dieser letzte Test sieht so aus:
DefaultTasksRepositoryTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {
private val task1 = Task("Title1", "Description1")
private val task2 = Task("Title2", "Description2")
private val task3 = Task("Title3", "Description3")
private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
private val localTasks = listOf(task3).sortedBy { it.id }
private val newTasks = listOf(task3).sortedBy { it.id }
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
@Before
fun createRepository() {
tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
// Get a reference to the class under test
tasksRepository = DefaultTasksRepository(
// TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
// this requires understanding more about coroutines + testing
// so we will keep this as Unconfined for now.
tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
)
}
@Test
fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
// When tasks are requested from the tasks repository
val tasks = tasksRepository.getTasks(true) as Success
// Then tasks are loaded from the remote data source
assertThat(tasks.data, IsEqual(remoteTasks))
}
}- Führen Sie den neuen
getTasks_requestsAllTasksFromRemoteDataSource-Test aus und prüfen Sie, ob er funktioniert und der Fehler behoben ist.
Sie haben gerade gesehen, wie Sie ein Repository unit-testen. In den nächsten Schritten verwenden Sie noch einmal die Abhängigkeitsinjektion und erstellen ein weiteres Test-Double. Diesmal wird gezeigt, wie Sie Unit- und Integrationstests für Ihre ViewModels schreiben.
Bei Einheitentests sollte nur die Klasse oder Methode getestet werden, die Sie interessiert. Dies wird als Isolation bezeichnet. Dabei wird die „Einheit“ klar isoliert und nur der Code getestet, der Teil dieser Einheit ist.
TasksViewModelTest sollte also nur TasksViewModel-Code testen und nicht in Datenbank-, Netzwerk- oder Repository-Klassen. Daher erstellen Sie für Ihre Ansichtsmodelle, wie gerade für Ihr Repository, ein gefälschtes Repository und wenden die Abhängigkeitsinjektion an, um es in Ihren Tests zu verwenden.
In dieser Aufgabe wenden Sie die Abhängigkeitsinjektion auf Ansichtsmodelle an.

Schritt 1: TasksRepository-Schnittstelle erstellen
Der erste Schritt zur Verwendung der Constructor Dependency Injection besteht darin, eine gemeinsame Schnittstelle zu erstellen, die von der Fake- und der echten Klasse verwendet wird.
Wie sieht das in der Praxis aus? Sehen Sie sich TasksRemoteDataSource, TasksLocalDataSource und FakeDataSource an. Sie alle haben dieselbe Schnittstelle: TasksDataSource. So können Sie im Konstruktor von DefaultTasksRepository angeben, dass Sie ein TasksDataSource verwenden.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {So können wir FakeDataSource einfügen.
Erstellen Sie als Nächstes eine Schnittstelle für DefaultTasksRepository, wie Sie es für die Datenquellen getan haben. Sie muss alle öffentlichen Methoden (öffentliche API-Oberfläche) von DefaultTasksRepository enthalten.
- Öffnen Sie
DefaultTasksRepositoryund klicken Sie mit der rechten Maustaste auf den Kursnamen. Wählen Sie dann Refactor -> Extract -> Interface (Umgestalten -> Extrahieren -> Schnittstelle) aus.

- Wählen Sie In separate Datei extrahieren aus.

- Ändern Sie im Fenster Schnittstelle extrahieren den Namen der Schnittstelle in
TasksRepository. - Setzen Sie im Bereich Mitglieder für die Formularschnittstelle ein Häkchen bei allen Mitgliedern außer den beiden Companion-Mitgliedern und den privaten Methoden.

- Klicken Sie auf Refactor (Umgestalten). Die neue
TasksRepository-Schnittstelle sollte im Paket data/source angezeigt werden.

Und DefaultTasksRepository implementiert jetzt TasksRepository.
- Führen Sie Ihre App aus (nicht die Tests), um zu prüfen, ob alles noch funktioniert.
Schritt 2: FakeTestRepository erstellen
Nachdem Sie die Schnittstelle haben, können Sie das DefaultTaskRepository-Test-Double erstellen.
- Erstellen Sie im test-Quellset in data/source die Kotlin-Datei und -Klasse
FakeTestRepository.ktund erweitern Sie sie über dieTasksRepository-Schnittstelle.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}Sie werden aufgefordert, die Schnittstellenmethoden zu implementieren.
- Bewegen Sie den Mauszeiger auf den Fehler, bis das Vorschlagsmenü angezeigt wird, klicken Sie dann auf Implement members (Mitglieder implementieren) und wählen Sie die Option aus.
- Wählen Sie alle Methoden aus und drücken Sie OK.

Schritt 3: FakeTestRepository-Methoden implementieren
Sie haben jetzt eine FakeTestRepository-Klasse mit Methoden, die nicht implementiert sind. Ähnlich wie bei der Implementierung von FakeDataSource wird FakeTestRepository von einer Datenstruktur unterstützt, anstatt dass eine komplizierte Vermittlung zwischen lokalen und Remote-Datenquellen erforderlich ist.
Für Ihr FakeTestRepository sind keine FakeDataSources oder Ähnliches erforderlich. Es muss lediglich realistische gefälschte Ausgaben für die Eingaben zurückgeben. Sie verwenden ein LinkedHashMap zum Speichern der Liste der Aufgaben und ein MutableLiveData für die beobachtbaren Aufgaben.
- Fügen Sie in
FakeTestRepositorysowohl eineLinkedHashMap-Variable für die aktuelle Liste der Aufgaben als auch eineMutableLiveDatafür die beobachtbaren Aufgaben hinzu.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}Implementieren Sie die folgenden Methoden:
getTasks: Diese Methode solltetasksServiceDatain eine Liste umwandeln (mittasksServiceData.values.toList()) und das Ergebnis alsSuccesszurückgeben.refreshTasks: Aktualisiert den Wert vonobservableTasksauf den Wert, der vongetTasks()zurückgegeben wird.observeTasks: Erstellt eine Coroutine mitrunBlockingund führtrefreshTasksaus. Gibt dannobservableTaskszurück.
Unten finden Sie den Code für diese Methoden.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
// Rest of class
}Schritt 4: Methode zum Testen von „addTasks“ hinzufügen
Beim Testen ist es besser, wenn sich bereits einige Tasks in Ihrem Repository befinden. Sie könnten saveTask mehrmals aufrufen. Um dies zu vereinfachen, fügen Sie eine Hilfsmethode speziell für Tests hinzu, mit der Sie Aufgaben hinzufügen können.
- Fügen Sie die Methode
addTaskshinzu, die einevarargmit Aufgaben entgegennimmt, jede Aufgabe derHashMaphinzufügt und dann die Aufgaben aktualisiert.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}Sie haben jetzt ein gefälschtes Repository zum Testen mit einigen der wichtigsten implementierten Methoden. Verwenden Sie sie dann in Ihren Tests.
In dieser Aufgabe verwenden Sie eine gefälschte Klasse in einem ViewModel. Verwenden Sie die Constructor Dependency Injection, um die beiden Datenquellen über die Constructor Dependency Injection zu übernehmen. Fügen Sie dazu dem Konstruktor von TasksViewModel eine TasksRepository-Variable hinzu.
Bei Viewmodels ist dieser Prozess etwas anders, da Sie sie nicht direkt erstellen. Beispiel:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Wie im obigen Code verwenden Sie den viewModel's Property-Delegate, der das ViewModel erstellt. Wenn Sie ändern möchten, wie das Ansichtsmodell erstellt wird, müssen Sie ein ViewModelProvider.Factory hinzufügen und verwenden. Wenn Sie ViewModelProvider.Factory noch nicht kennen, finden Sie hier weitere Informationen.
Schritt 1: ViewModelFactory in TasksViewModel erstellen und verwenden
Beginnen Sie mit der Aktualisierung der Klassen und Tests für den Bildschirm Tasks.
TasksViewModelöffnen- Ändern Sie den Konstruktor von
TasksViewModelso, dass erTasksRepositoryakzeptiert, anstatt ihn in der Klasse zu erstellen.
TasksViewModel.kt
// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() {
// Rest of class
}Da Sie den Konstruktor geändert haben, müssen Sie jetzt eine Factory verwenden, um TasksViewModel zu erstellen. Die Factory-Klasse kann in derselben Datei wie TasksViewModel oder in einer eigenen Datei gespeichert werden.
- Fügen Sie unten in der Datei
TasksViewModelaußerhalb der Klasse einTasksViewModelFactoryein, das ein einfachesTasksRepositoryakzeptiert.
TasksViewModel.kt
@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TasksViewModel(tasksRepository) as T)
}
So ändern Sie die Konstruktion von ViewModel. Jetzt, da Sie die Factory haben, können Sie sie überall verwenden, wo Sie Ihr View-Modell erstellen.
TasksFragmentaktualisieren, um die Factory zu verwenden
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- Führen Sie den Code Ihrer App aus und prüfen Sie, ob alles noch funktioniert.
Schritt 2: FakeTestRepository in TasksViewModelTest verwenden
Anstelle des echten Repositorys können Sie jetzt das gefälschte Repository in Ihren Viewmodel-Tests verwenden.
- Öffnen Sie
TasksViewModelTest. - Fügen Sie dem
TasksViewModelTestdas AttributFakeTestRepositoryhinzu.
TaskViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Use a fake repository to be injected into the viewmodel
private lateinit var tasksRepository: FakeTestRepository
// Rest of class
}- Aktualisieren Sie die Methode
setupViewModel, um eineFakeTestRepositorymit drei Aufgaben zu erstellen, und erstellen Sie dann dastasksViewModelmit diesem Repository.
TasksViewModelTest.kt
@Before
fun setupViewModel() {
// We initialise the tasks to 3, with one active and two completed
tasksRepository = FakeTestRepository()
val task1 = Task("Title1", "Description1")
val task2 = Task("Title2", "Description2", true)
val task3 = Task("Title3", "Description3", true)
tasksRepository.addTasks(task1, task2, task3)
tasksViewModel = TasksViewModel(tasksRepository)
}- Da Sie den AndroidX Test-Code
ApplicationProvider.getApplicationContextnicht mehr verwenden, können Sie auch die Annotation@RunWith(AndroidJUnit4::class)entfernen. - Führen Sie Ihre Tests aus und prüfen Sie, ob sie alle noch funktionieren.
Durch die Verwendung der Constructor-Dependency-Injection haben Sie die DefaultTasksRepository als Abhängigkeit entfernt und in den Tests durch Ihre FakeTestRepository ersetzt.
Schritt 3: Auch TaskDetail-Fragment und -ViewModel aktualisieren
Nehmen Sie genau dieselben Änderungen für TaskDetailFragment und TaskDetailViewModel vor. So wird der Code für die TaskDetail-Tests vorbereitet, die Sie als Nächstes schreiben.
TaskDetailViewModelöffnen- Aktualisieren Sie den Konstruktor:
TaskDetailViewModel.kt
// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// Rest of class
}
// WITH
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }- Fügen Sie unten in der Datei
TaskDetailViewModelaußerhalb der Klasse eineTaskDetailViewModelFactoryhinzu.
TaskDetailViewModel.kt
@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>) =
(TaskDetailViewModel(tasksRepository) as T)
}TasksFragmentaktualisieren, um die Factory zu verwenden
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}- Führen Sie Ihren Code aus und prüfen Sie, ob alles funktioniert.
Sie können jetzt in TasksFragment und TasksDetailFragment ein FakeTestRepository anstelle des tatsächlichen Repositorys verwenden.
Als Nächstes schreiben Sie Integrationstests, um die Interaktionen zwischen Fragment und ViewModel zu testen. Sie erfahren, ob Ihr Viewmodel-Code die Benutzeroberfläche richtig aktualisiert. Dazu verwenden Sie
- das ServiceLocator-Muster
- Espresso- und Mockito-Bibliotheken
Integrationstests prüfen die Interaktion mehrerer Klassen, um sicherzustellen, dass sie sich bei gemeinsamer Verwendung wie erwartet verhalten. Diese Tests können entweder lokal (test-Quellgruppe) oder als Instrumentierungstests (androidTest-Quellgruppe) ausgeführt werden.

In Ihrem Fall nehmen Sie jedes Fragment und schreiben Integrationstests für das Fragment und das View-Modell, um die Hauptfunktionen des Fragments zu testen.
Schritt 1: Gradle-Abhängigkeiten hinzufügen
- Fügen Sie die folgenden Gradle-Abhängigkeiten hinzu.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "junit:junit:$junitVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
// Testing code should not be included in the main code.
// Once https://issuetracker.google.com/128612536 is fixed this can be fixed.
implementation "androidx.fragment:fragment-testing:$fragmentVersion"
implementation "androidx.test:core:$androidXTestCoreVersion"
Dazu gehören:
junit:junit: JUnit, das zum Schreiben grundlegender Testanweisungen erforderlich ist.androidx.test:core– Core AndroidX-Testbibliothekkotlinx-coroutines-test– Die Coroutines-Testbibliothekandroidx.fragment:fragment-testing: AndroidX-Testbibliothek zum Erstellen von Fragmenten in Tests und zum Ändern ihres Status.
Da Sie diese Bibliotheken in Ihrem androidTest-Quellsatz verwenden, fügen Sie sie mit androidTestImplementation als Abhängigkeiten hinzu.
Schritt 2: Klasse „TaskDetailFragmentTest“ erstellen
Die TaskDetailFragment enthält Informationen zu einer einzelnen Aufgabe.

Sie beginnen mit dem Schreiben eines Fragmenttests für TaskDetailFragment, da es im Vergleich zu den anderen Fragmenten relativ einfache Funktionen hat.
taskdetail.TaskDetailFragmentöffnen- Generieren Sie einen Test für
TaskDetailFragmentwie zuvor. Akzeptieren Sie die Standardauswahl und legen Sie sie im Quellsatz androidTest ab (NICHT im Quellsatztest).

- Fügen Sie der Klasse
TaskDetailFragmentTestdie folgenden Annotationen hinzu.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}Der Zweck dieser Anmerkungen ist:
@MediumTest: Kennzeichnet den Test als Integrationstest mit mittlerer Laufzeit (im Gegensatz zu@SmallTest-Unit-Tests und@LargeTest-End-to-End-Tests). So können Sie die Tests gruppieren und die Größe des Tests auswählen, den Sie ausführen möchten.@RunWith(AndroidJUnit4::class): Wird in jeder Klasse verwendet, in der AndroidX Test verwendet wird.
Schritt 3: Fragment über einen Test starten
In dieser Aufgabe starten Sie TaskDetailFragment mit der AndroidX Testing-Bibliothek. FragmentScenario ist eine Klasse aus AndroidX Test, die ein Fragment umschließt und Ihnen die direkte Steuerung des Fragmentlebenszyklus für Tests ermöglicht. Um Tests für Fragmente zu schreiben, erstellen Sie ein FragmentScenario für das Fragment, das Sie testen (TaskDetailFragment).
- Kopieren Sie diesen Test in
TaskDetailFragmentTest.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() {
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Dieser Code oben:
- Erstellt eine Aufgabe.
- Erstellt ein
Bundle, das die Fragmentargumente für die Aufgabe darstellt, die an das Fragment übergeben werden. - Die Funktion
launchFragmentInContainererstellt mit diesem Bundle und einem Design einFragmentScenario.
Das ist noch kein vollständiger Test, da nichts bestätigt wird. Führen Sie den Test erst einmal aus und beobachten Sie, was passiert.
- Da es sich um einen instrumentierten Test handelt, muss der Emulator oder Ihr Gerät sichtbar sein.
- Führen Sie den Test aus.
Es sollten einige Dinge passieren.
- Da es sich um einen instrumentierten Test handelt, wird er entweder auf Ihrem physischen Gerät (falls verbunden) oder auf einem Emulator ausgeführt.
- Dadurch sollte das Fragment gestartet werden.
- Es wird kein anderes Fragment aufgerufen und es sind keine Menüs mit der Aktivität verknüpft – es ist nur das Fragment.
Sehen Sie sich das Fragment genau an. Es wird „Keine Daten“ angezeigt, da die Aufgabendaten nicht geladen werden konnten.

In Ihrem Test muss sowohl TaskDetailFragment geladen als auch bestätigt werden, dass die Daten korrekt geladen wurden. Warum sind keine Daten vorhanden? Das liegt daran, dass Sie eine Aufgabe erstellt, aber nicht im Repository gespeichert haben.
@Test
fun activeTaskDetails_DisplayedInUi() {
// This DOES NOT save the task anywhere
val activeTask = Task("Active Task", "AndroidX Rocks", false)
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
Sie haben dieses FakeTestRepository, aber Sie benötigen eine Möglichkeit, Ihr echtes Repository für Ihr Fragment durch das gefälschte zu ersetzen. Das ist der nächste Schritt.
In dieser Aufgabe stellen Sie Ihrem Fragment Ihr gefälschtes Repository über ein ServiceLocator zur Verfügung. So können Sie Ihre Fragment- und Viewmodel-Integrationstests schreiben.
Sie können hier keine Constructor Dependency Injection verwenden, wie Sie es zuvor getan haben, als Sie eine Abhängigkeit für das View-Modell oder das Repository bereitstellen mussten. Für die Constructor Dependency Injection muss die Klasse erstellt werden. Fragmente und Aktivitäten sind Beispiele für Klassen, die Sie nicht erstellen und auf deren Konstruktor Sie in der Regel keinen Zugriff haben.
Da Sie das Fragment nicht erstellen, können Sie die Constructor Dependency Injection nicht verwenden, um das Repository-Test-Double (FakeTestRepository) mit dem Fragment zu tauschen. Verwenden Sie stattdessen das Service Locator-Muster. Das Service Locator-Muster ist eine Alternative zur Abhängigkeitsinjektion. Dazu wird eine Singleton-Klasse namens „Service Locator“ erstellt, die sowohl für den regulären als auch für den Testcode Abhängigkeiten bereitstellt. Im regulären App-Code (der main-Quellsatz) sind alle diese Abhängigkeiten die regulären App-Abhängigkeiten. Für die Tests ändern Sie den Service Locator, um Test-Double-Versionen der Abhängigkeiten bereitzustellen.
Service Locator nicht verwenden
| Service Locator verwenden
|
Gehen Sie für diese Codelab-App so vor:
- Erstellen Sie eine Service Locator-Klasse, die ein Repository erstellen und speichern kann. Standardmäßig wird ein „normales“ Repository erstellt.
- Lagern Sie Ihren Code so um, dass der Service Locator verwendet wird, wenn Sie ein Repository benötigen.
- Rufen Sie in Ihrer Testklasse eine Methode für den Service Locator auf, die das „normale“ Repository durch Ihr Test-Double ersetzt.
Schritt 1: ServiceLocator erstellen
Erstellen wir eine ServiceLocator-Klasse. Sie befindet sich mit dem restlichen App-Code im Hauptquellsatz, da sie vom Hauptanwendungscode verwendet wird.
Hinweis: ServiceLocator ist ein Singleton. Verwenden Sie daher das Kotlin-Schlüsselwort object für die Klasse.
- Erstellen Sie die Datei ServiceLocator.kt auf der obersten Ebene des Hauptquellsets.
- Definieren Sie eine
objectmit dem NamenServiceLocator. - Erstellen Sie die Instanzvariablen
databaseundrepositoryund legen Sie beide aufnullfest. - Kommentieren Sie das Repository mit
@Volatile, da es von mehreren Threads verwendet werden könnte (@Volatilewird hier ausführlich erläutert).
Ihr Code sollte wie unten aussehen.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}Derzeit muss Ihre ServiceLocator nur wissen, wie eine TasksRepository zurückgegeben wird. Es wird ein vorhandenes DefaultTasksRepository zurückgegeben oder bei Bedarf ein neues DefaultTasksRepository erstellt und zurückgegeben.
Definieren Sie die folgenden Funktionen:
provideTasksRepository: Stellt entweder ein bereits vorhandenes Repository bereit oder erstellt ein neues. Diese Methode solltesynchronizedfürthissein, um zu vermeiden, dass in Situationen mit mehreren ausgeführten Threads versehentlich zwei Repository-Instanzen erstellt werden.createTasksRepository: Code zum Erstellen eines neuen Repositorys. RuftcreateTaskLocalDataSourceauf und erstellt eine neueTasksRemoteDataSource.createTaskLocalDataSource: Code zum Erstellen einer neuen lokalen Datenquelle.createDataBasewird angerufen.createDataBase: Code zum Erstellen einer neuen Datenbank.
Der vollständige Code ist unten zu sehen.
ServiceLocator.kt
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
fun provideTasksRepository(context: Context): TasksRepository {
synchronized(this) {
return tasksRepository ?: createTasksRepository(context)
}
}
private fun createTasksRepository(context: Context): TasksRepository {
val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
tasksRepository = newRepo
return newRepo
}
private fun createTaskLocalDataSource(context: Context): TasksDataSource {
val database = database ?: createDataBase(context)
return TasksLocalDataSource(database.taskDao())
}
private fun createDataBase(context: Context): ToDoDatabase {
val result = Room.databaseBuilder(
context.applicationContext,
ToDoDatabase::class.java, "Tasks.db"
).build()
database = result
return result
}
}Schritt 2: ServiceLocator in der Anwendung verwenden
Sie nehmen eine Änderung an Ihrem Hauptanwendungscode (nicht an Ihren Tests) vor, damit Sie das Repository an einem Ort erstellen, nämlich in ServiceLocator.
Es ist wichtig, dass Sie immer nur eine Instanz der Repository-Klasse erstellen. Dazu verwenden Sie den Service Locator in Ihrer Application-Klasse.
- Öffnen Sie
TodoApplicationauf der obersten Ebene Ihrer Pakethierarchie und erstellen Sie einvalfür Ihr Repository. Weisen Sie ihm ein Repository zu, das mitServiceLocator.provideTaskRepositoryabgerufen wird.
TodoApplication.kt
class TodoApplication : Application() {
val taskRepository: TasksRepository
get() = ServiceLocator.provideTasksRepository(this)
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
}
}
Nachdem Sie ein Repository in der Anwendung erstellt haben, können Sie die alte getRepository-Methode in DefaultTasksRepository entfernen.
- Öffnen Sie
DefaultTasksRepositoryund löschen Sie das Companion-Objekt.
DefaultTasksRepository.kt
// DELETE THIS COMPANION OBJECT
companion object {
@Volatile
private var INSTANCE: DefaultTasksRepository? = null
fun getRepository(app: Application): DefaultTasksRepository {
return INSTANCE ?: synchronized(this) {
val database = Room.databaseBuilder(app,
ToDoDatabase::class.java, "Tasks.db")
.build()
DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
INSTANCE = it
}
}
}
}Verwenden Sie jetzt überall, wo Sie getRepository verwendet haben, stattdessen taskRepository der Anwendung. So wird sichergestellt, dass Sie nicht das Repository direkt erstellen, sondern das Repository erhalten, das vom ServiceLocator bereitgestellt wurde.
- Öffnen Sie
TaskDetailFragementund suchen Sie oben in der Klasse nach dem Aufruf vongetRepository. - Ersetzen Sie diesen Aufruf durch einen Aufruf, der das Repository von
TodoApplicationabruft.
TaskDetailFragment.kt
// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- Wiederholen Sie diesen Schritt für
TasksFragment.
TasksFragment.kt
// REPLACE this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
// WITH this code
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}- Aktualisieren Sie für
StatisticsViewModelundAddEditTaskViewModelden Code, mit dem das Repository abgerufen wird, sodass das Repository ausTodoApplicationverwendet wird.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Führen Sie Ihre Anwendung (nicht den Test) aus.
Da Sie nur Refactoring durchgeführt haben, sollte die App problemlos laufen.
Schritt 3: FakeAndroidTestRepository erstellen
Sie haben bereits ein FakeTestRepository im Testquellenset. Testklassen können standardmäßig nicht zwischen den Quellsätzen test und androidTest geteilt werden. Sie müssen also eine doppelte FakeTestRepository-Klasse im Quellset androidTest erstellen und sie FakeAndroidTestRepository nennen.
- Klicken Sie mit der rechten Maustaste auf das Quellset
androidTestund erstellen Sie ein Datenpaket. Klicken Sie noch einmal mit der rechten Maustaste und erstellen Sie ein Quellpaket . - Erstellen Sie in diesem Quellpaket eine neue Klasse mit dem Namen
FakeAndroidTestRepository.kt. - Kopieren Sie den folgenden Code in diese Klasse.
FakeAndroidTestRepository.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap
class FakeAndroidTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private var shouldReturnError = false
private val observableTasks = MutableLiveData<Result<List<Task>>>()
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override suspend fun refreshTasks() {
observableTasks.value = getTasks()
}
override suspend fun refreshTask(taskId: String) {
refreshTasks()
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
runBlocking { refreshTasks() }
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { refreshTasks() }
return observableTasks.map { tasks ->
when (tasks) {
is Result.Loading -> Result.Loading
is Error -> Error(tasks.exception)
is Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return@map Error(Exception("Not found"))
Success(task)
}
}
}
}
override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
tasksServiceData[taskId]?.let {
return Success(it)
}
return Error(Exception("Could not find task"))
}
override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
if (shouldReturnError) {
return Error(Exception("Test exception"))
}
return Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun completeTask(task: Task) {
val completedTask = Task(task.title, task.description, true, task.id)
tasksServiceData[task.id] = completedTask
}
override suspend fun completeTask(taskId: String) {
// Not required for the remote data source.
throw NotImplementedError()
}
override suspend fun activateTask(task: Task) {
val activeTask = Task(task.title, task.description, false, task.id)
tasksServiceData[task.id] = activeTask
}
override suspend fun activateTask(taskId: String) {
throw NotImplementedError()
}
override suspend fun clearCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.isCompleted
} as LinkedHashMap<String, Task>
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
refreshTasks()
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
refreshTasks()
}
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
}
Schritt 4: ServiceLocator für Tests vorbereiten
Okay, jetzt ist es an der Zeit, ServiceLocator zu verwenden, um beim Testen Test-Doubles einzusetzen. Dazu müssen Sie Ihrem ServiceLocator-Code Code hinzufügen.
ServiceLocator.ktöffnen- Markieren Sie den Setter für
tasksRepositoryals@VisibleForTesting. Mit dieser Anmerkung wird ausgedrückt, dass der Setter aus Testzwecken öffentlich ist.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting setUnabhängig davon, ob Sie Ihren Test allein oder in einer Gruppe von Tests ausführen, sollten die Tests genau gleich ablaufen. Das bedeutet, dass Ihre Tests kein Verhalten aufweisen sollten, das voneinander abhängig ist. Sie sollten also keine Objekte zwischen Tests freigeben.
Da ServiceLocator ein Singleton ist, kann es versehentlich zwischen Tests geteilt werden. Um dies zu vermeiden, erstellen Sie eine Methode, die den ServiceLocator-Status zwischen den Tests richtig zurücksetzt.
- Fügen Sie eine Instanzvariable mit dem Namen
lockund dem WertAnyhinzu.
ServiceLocator.kt
private val lock = Any()- Fügen Sie eine testspezifische Methode namens
resetRepositoryhinzu, die die Datenbank leert und sowohl das Repository als auch die Datenbank auf „null“ setzt.
ServiceLocator.kt
@VisibleForTesting
fun resetRepository() {
synchronized(lock) {
runBlocking {
TasksRemoteDataSource.deleteAllTasks()
}
// Clear all data to avoid test pollution.
database?.apply {
clearAllTables()
close()
}
database = null
tasksRepository = null
}
}Schritt 5: ServiceLocator verwenden
In diesem Schritt verwenden Sie ServiceLocator.
TaskDetailFragmentTestöffnen- Deklarieren Sie eine
lateinit TasksRepository-Variable. - Fügen Sie eine Setup- und eine Tear-Down-Methode hinzu, um vor jedem Test eine
FakeAndroidTestRepositoryeinzurichten und sie nach jedem Test zu bereinigen.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Schließen Sie den Funktionskörper von
activeTaskDetails_DisplayedInUi()inrunBlockingTestein. - Speichern Sie
activeTaskim Repository, bevor Sie das Fragment starten.
repository.saveTask(activeTask)Der endgültige Test sieht so aus:
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}- Kommentieren Sie die gesamte Klasse mit
@ExperimentalCoroutinesApi.
Wenn Sie fertig sind, sieht der Code so aus.
TaskDetailFragmentTest.kt
@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
}
}
- Führen Sie den
activeTaskDetails_DisplayedInUi()-Test aus.
Wie zuvor sollte das Fragment angezeigt werden. Da Sie das Repository jedoch richtig eingerichtet haben, werden jetzt die Aufgabeninformationen angezeigt.

In diesem Schritt verwenden Sie die Espresso-UI-Testbibliothek, um Ihren ersten Integrationstest durchzuführen. Sie haben Ihren Code so strukturiert, dass Sie Tests mit Zusicherungen für Ihre Benutzeroberfläche hinzufügen können. Dazu verwenden Sie die Espresso-Testbibliothek.
Espresso bietet folgende Vorteile:
- Interagieren Sie mit Ansichten, z. B. indem Sie auf Schaltflächen klicken, einen Balken verschieben oder auf einem Bildschirm nach unten scrollen.
- Bestätigen, dass bestimmte Ansichten auf dem Bildschirm angezeigt werden oder sich in einem bestimmten Zustand befinden (z. B. dass sie bestimmten Text enthalten oder dass ein Kästchen angekreuzt ist).
Schritt 1: Gradle-Abhängigkeit
Die Hauptabhängigkeit von Espresso ist bereits vorhanden, da sie standardmäßig in Android-Projekte aufgenommen wird.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}androidx.test.espresso:espresso-core: Diese Espresso-Kernabhängigkeit ist standardmäßig enthalten, wenn Sie ein neues Android-Projekt erstellen. Sie enthält den grundlegenden Testcode für die meisten Ansichten und Aktionen.
Schritt 2: Animationen deaktivieren
Espresso-Tests werden auf einem echten Gerät ausgeführt und sind daher Instrumentierungstests. Ein Problem, das auftreten kann, sind Animationen: Wenn eine Animation verzögert wird und Sie versuchen, zu testen, ob eine Ansicht auf dem Bildschirm angezeigt wird, sie aber noch animiert wird, kann Espresso einen Test versehentlich fehlschlagen lassen. Das kann dazu führen, dass Espresso-Tests instabil werden.
Für Espresso-UI-Tests empfiehlt es sich, Animationen zu deaktivieren. Dadurch werden Ihre Tests auch schneller ausgeführt:
- Rufen Sie auf Ihrem Testgerät die Einstellungen > Entwickleroptionen auf.
- Deaktivieren Sie die drei Einstellungen Fensteranimationsfaktor, Übergangsanimationsfaktor und Animationsdauerfaktor.

Schritt 3: Espresso-Test ansehen
Bevor Sie einen Espresso-Test schreiben, sollten Sie sich etwas Espresso-Code ansehen.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))Mit dieser Anweisung wird die Checkbox-Ansicht mit der ID task_detail_complete_checkbox gesucht, darauf geklickt und dann bestätigt, dass sie aktiviert ist.
Die meisten Espresso-Anweisungen bestehen aus vier Teilen:
onViewonView ist ein Beispiel für eine statische Espresso-Methode, mit der eine Espresso-Anweisung gestartet wird. onView ist eine der häufigsten, aber es gibt auch andere Optionen wie onData.
2. ViewMatcher
withId(R.id.task_detail_title_text)withId ist ein Beispiel für eine ViewMatcher, die eine Ansicht anhand ihrer ID abruft. Es gibt weitere View-Matcher, die Sie in der Dokumentation nachschlagen können.
3. ViewAction
perform(click())Die Methode perform, die ein ViewAction akzeptiert. Eine ViewAction ist eine Aktion, die für die Ansicht ausgeführt werden kann, z. B. das Klicken auf die Ansicht.
check(matches(isChecked()))check, das ein ViewAssertion akzeptiert. ViewAssertions check or asserts something about the view. Die am häufigsten verwendete ViewAssertion ist die matches-Assertion. Verwenden Sie zum Abschließen der Assertion ein anderes ViewMatcher, in diesem Fall isChecked.

Beachten Sie, dass Sie in einer Espresso-Anweisung nicht immer sowohl perform als auch check aufrufen. Sie können Anweisungen haben, die nur eine Behauptung mit check aufstellen, oder nur eine ViewAction mit perform ausführen.
TaskDetailFragmentTest.ktöffnen- Aktualisieren Sie den
activeTaskDetails_DisplayedInUi-Test.
TaskDetailFragmentTest.kt
@Test
fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add active (incomplete) task to the DB
val activeTask = Task("Active Task", "AndroidX Rocks", false)
repository.saveTask(activeTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
}
Hier sind die Importanweisungen, falls erforderlich:
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not- Alles nach dem
// THEN-Kommentar verwendet Espresso. Sehen Sie sich die Teststruktur und die Verwendung vonwithIdan und treffen Sie Aussagen dazu, wie die Detailseite aussehen sollte. - Führen Sie den Test aus und bestätigen Sie, dass er bestanden wurde.
Schritt 4: Optional: Eigenen Espresso-Test schreiben
Schreiben Sie nun selbst einen Test.
- Erstellen Sie einen neuen Test mit dem Namen
completedTaskDetails_DisplayedInUiund kopieren Sie diesen Skelettcode.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
// WHEN - Details fragment launched to display task
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
}- Sehen Sie sich den vorherigen Test an und schließen Sie diesen Test ab.
- Führen Sie den Test aus und bestätigen Sie, dass er bestanden wurde.
Die fertige completedTaskDetails_DisplayedInUi sollte so aussehen.
TaskDetailFragmentTest.kt
@Test
fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
// GIVEN - Add completed task to the DB
val completedTask = Task("Completed Task", "AndroidX Rocks", true)
repository.saveTask(completedTask)
// WHEN - Details fragment launched to display task
val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)
// THEN - Task details are displayed on the screen
// make sure that the title/description are both shown and correct
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
// and make sure the "active" checkbox is shown unchecked
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
}In diesem letzten Schritt erfahren Sie, wie Sie die Navigation-Komponente mit einem anderen Typ von Test-Double, einem Mock, und der Testbibliothek Mockito testen.
In diesem Codelab haben Sie ein Test-Double namens „Fake“ verwendet. Fakes sind eine von vielen Arten von Test-Doubles. Welches Test-Double sollten Sie zum Testen der Navigation Component verwenden?
Überlegen Sie, wie die Navigation erfolgt. Stellen Sie sich vor, Sie drücken eine der Aufgaben in TasksFragment, um den Bildschirm mit den Aufgabendetails aufzurufen.

Hier ist der Code in TasksFragment, der bei einem Tastendruck zum Bildschirm mit den Aufgabendetails wechselt.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
Die Navigation erfolgt aufgrund eines Aufrufs der Methode navigate. Wenn Sie eine Assert-Anweisung schreiben mussten, gibt es keine einfache Möglichkeit, zu testen, ob Sie zu TaskDetailFragment navigiert sind. Die Navigation ist eine komplizierte Aktion, die über die Initialisierung von TaskDetailFragment hinaus nicht zu einer klaren Ausgabe oder Zustandsänderung führt.
Sie können bestätigen, dass die Methode navigate mit dem richtigen Aktionsparameter aufgerufen wurde. Genau das macht ein Mock-Test-Double: Es prüft, ob bestimmte Methoden aufgerufen wurden.
Mockito ist ein Framework zum Erstellen von Test-Doubles. Obwohl das Wort „mock“ in der API und im Namen verwendet wird, dient es nicht nur zum Erstellen von Mockups. Außerdem können Stubs und Spies erstellt werden.
Sie verwenden Mockito, um ein Mock-Objekt für NavigationController zu erstellen, mit dem Sie bestätigen können, dass die Methode „navigate“ korrekt aufgerufen wurde.
Schritt 1: Gradle-Abhängigkeiten hinzufügen
- Fügen Sie die Gradle-Abhängigkeiten hinzu.
app/build.gradle
// Dependencies for Android instrumented unit tests
androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"
androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
org.mockito:mockito-core: Dies ist die Mockito-Abhängigkeit.dexmaker-mockito: Diese Bibliothek ist erforderlich, um Mockito in einem Android-Projekt zu verwenden. Mockito muss Klassen zur Laufzeit generieren. Unter Android erfolgt dies mit DEX-Bytecode. Mit dieser Bibliothek kann Mockito also Objekte zur Laufzeit unter Android generieren.androidx.test.espresso:espresso-contrib: Diese Bibliothek besteht aus externen Beiträgen (daher der Name), die Testcode für komplexere Ansichten wieDatePickerundRecyclerViewenthalten. Es enthält auch Bedienungshilfen-Prüfungen und die KlasseCountingIdlingResource, die später behandelt wird.
Schritt 2: TasksFragmentTest erstellen
- Öffnen Sie
TasksFragment. - Klicken Sie mit der rechten Maustaste auf den Klassennamen
TasksFragmentund wählen Sie Generate (Generieren) und dann Test (Test) aus. Erstellen Sie einen Test im Quellsatz androidTest. - Kopieren Sie diesen Code in die Datei
TasksFragmentTest.
TasksFragmentTest.kt
@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
}Dieser Code ähnelt dem TaskDetailFragmentTest-Code, den Sie geschrieben haben. Es richtet ein FakeAndroidTestRepository ein und baut es wieder ab. Fügen Sie einen Navigationstest hinzu, um zu prüfen, ob Sie beim Klicken auf eine Aufgabe in der Aufgabenliste zum richtigen TaskDetailFragment weitergeleitet werden.
- Fügen Sie den Test
clickTask_navigateToDetailFragmentOnehinzu.
TasksFragmentTest.kt
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
}
- Verwenden Sie die Mockito-Funktion
mock, um ein Mock-Objekt zu erstellen.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)Um in Mockito ein Mock-Objekt zu erstellen, übergeben Sie die Klasse, die Sie simulieren möchten.
Als Nächstes müssen Sie Ihr NavController mit dem Fragment verknüpfen. Mit onFragment können Sie Methoden für das Fragment selbst aufrufen.
- Machen Sie das neue Mock zum
NavControllerdes Fragments.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}- Fügen Sie den Code hinzu, um auf das Element in
RecyclerViewzu klicken, das den Text „TITLE1“ enthält.
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))RecyclerViewActions ist Teil der espresso-contrib-Bibliothek und ermöglicht es Ihnen, Espresso-Aktionen für eine RecyclerView auszuführen.
- Prüfen Sie, ob
navigatemit dem richtigen Argument aufgerufen wurde.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")Die Mockito-Methode verify macht dies zu einem Mock. Sie können bestätigen, dass für das gemockte navController eine bestimmte Methode (navigate) mit einem Parameter (actionTasksFragmentToTaskDetailFragment mit der ID „id1“) aufgerufen wurde.
Der vollständige Test sieht so aus:
@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the first list item
onView(withId(R.id.tasks_list))
.perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText("TITLE1")), click()))
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
)
}- Test starten
Zusammenfassend lässt sich sagen, dass Sie die Navigation so testen können:
- Verwenden Sie Mockito, um ein
NavController-Mock zu erstellen. - Hängen Sie das simulierte
NavControlleran das Fragment an. - Prüfen Sie, ob „navigate“ mit der richtigen Aktion und den richtigen Parametern aufgerufen wurde.
Schritt 3: Optional: write clickAddTaskButton_navigateToAddEditFragment
Wenn Sie selbst einen Navigationstest schreiben möchten, versuchen Sie es mit dieser Aufgabe.
- Schreibe den Test
clickAddTaskButton_navigateToAddEditFragment, der prüft, ob du durch Klicken auf das + FAB zurAddEditTaskFragmentgelangst.
Die Antwort finden Sie unten.
TasksFragmentTest.kt
@Test
fun clickAddTaskButton_navigateToAddEditFragment() {
// GIVEN - On the home screen
val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
val navController = mock(NavController::class.java)
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
// WHEN - Click on the "+" button
onView(withId(R.id.add_task_fab)).perform(click())
// THEN - Verify that we navigate to the add screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
null, getApplicationContext<Context>().getString(R.string.add_task)
)
)
}Klicken Sie hier, um einen Vergleich zwischen dem Code, mit dem Sie begonnen haben, und dem endgültigen Code zu sehen.
Wenn Sie den Code für das fertige Codelab herunterladen möchten, können Sie den folgenden Git-Befehl verwenden:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_2
Alternativ können Sie das Repository als ZIP-Datei herunterladen, entzippen und in Android Studio öffnen.
In diesem Codelab haben Sie gelernt, wie Sie die manuelle Abhängigkeitsinjektion und einen Service Locator einrichten und wie Sie Fakes und Mocks in Ihren Android-Kotlin-Apps verwenden. Wichtig ist insbesondere:
- Welche Tests Sie für Ihre App implementieren, hängt davon ab, was Sie testen möchten, und von Ihrer Teststrategie. Unittests sind fokussiert und schnell. Integrationstests prüfen die Interaktion zwischen Teilen Ihres Programms. End-to-End-Tests überprüfen Funktionen, haben die höchste Genauigkeit, werden oft instrumentiert und können länger dauern.
- Die Architektur Ihrer App beeinflusst, wie schwierig es ist, sie zu testen.
- TDD oder Test-Driven Development ist eine Strategie, bei der Sie zuerst die Tests schreiben und dann die Funktion erstellen, damit die Tests bestanden werden.
- Um Teile Ihrer App für Tests zu isolieren, können Sie Test-Doubles verwenden. Ein Test-Double ist eine Version einer Klasse, die speziell für Tests entwickelt wurde. Sie geben beispielsweise vor, Daten aus einer Datenbank oder dem Internet zu erhalten.
- Verwenden Sie Dependency Injection, um eine echte Klasse durch eine Testklasse zu ersetzen, z. B. ein Repository oder eine Netzwerkschicht.
- Verwenden Sie instrumentierte Tests (
androidTest), um UI-Komponenten zu starten. - Wenn Sie die Constructor-Dependency-Injection nicht verwenden können, z. B. zum Starten eines Fragments, können Sie häufig einen Service Locator verwenden. Das Service Locator-Muster ist eine Alternative zur Abhängigkeitsinjektion. Dazu wird eine Singleton-Klasse namens „Service Locator“ erstellt, die sowohl für den regulären als auch für den Testcode Abhängigkeiten bereitstellt.
Udacity-Kurs:
Android-Entwicklerdokumentation:
- Leitfaden zur App-Architektur
runBlockingundrunBlockingTestFragmentScenario- Espresso
- Mockito
- JUnit4
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- Quellsets
- Tests über die Befehlszeile durchführen
Videos:
Sonstiges:
Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Codelabs zum Thema „Android für Fortgeschrittene mit Kotlin“.




