Dieses Codelab ist Teil des Kurses „Advanced Android in Kotlin“. Sie profitieren von diesem Kurs, wenn Sie die Codelabs nacheinander durcharbeiten. Das ist aber nicht zwingend erforderlich. Alle Kurs-Codelabs finden Sie auf der Landingpage für Codelabs auf Android-Geräten für Fortgeschrittene.
Einführung
Im zweiten Test-Codelab geht es um Test-Doubles: Wann diese in Android verwendet werden und wie sie mithilfe von Abhängigkeitseinschleusungen, Service Locator-Muster und Bibliotheken implementiert werden. So lernen Sie:
- Repository-Unittests
- Fragmente und Integrationsmodelle nach Modell
- Fragmentnavigation
Was Sie bereits wissen sollten
Sie sollten mit Folgendem vertraut sein:
- Die Kotlin-Programmiersprache
- Im ersten Codelab behandelte Testkonzepte: Schreiben und Ausführen von Unit-Tests unter Android mit JUnit, Hamcrest, AndroidX-Test, Robolectric und LiveData testen
- Die folgenden Android-Jetpack-Bibliotheken:
ViewModel
,LiveData
und die Navigationskomponente - Anwendungsarchitektur gemäß dem Muster aus dem Leitfaden zur App-Architektur und den Codelabs für Android-Grundlagen
- Koroutinen unter Android
Lerninhalte
- Planung einer Teststrategie
- Test-Doubles, Fälschungen und Mocks, erstellen und verwenden
- Manuelle Abhängigkeitsinjektion in Android für Unit- und Integrationstests verwenden
- Muster für die Dienstsuche anwenden
- Repositories, Fragmente, Modelle und Navigationskomponente testen
Sie werden die folgenden Bibliotheken und Codekonzepte verwenden:
Aufgaben
- Unittests für ein Repository mit einem Test-Double und der Abhängigkeitsinjektion schreiben.
- Unittests für ein Datenansichtsmodell mit einem Test-Double und der Abhängigkeitsinjektion schreiben.
- Integrationstests für Fragmente und ihre Darstellungsmodelle mit dem Espresso-UI-Testframework schreiben.
- Navigationstests mit Mockito und Espresso schreiben
In dieser Codelab-Reihe arbeiten Sie mit der To-do-Notes-App. Sie können damit Aufgaben erledigen und in einer Liste anzeigen lassen. Diese können Sie dann als abgeschlossen markieren, sie filtern oder löschen.
Diese App wurde in Kotlin geschrieben, hat einige Bildschirme, verwendet Jetpack-Komponenten und folgt der Architektur eines Leitfadens zur App-Architektur. Wenn du lernst, wie du diese App testest, kannst du Apps testen, die dieselben Bibliotheken und Architekturen verwenden.
Code herunterladen
Lade den Code herunter, um loszulegen:
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
Nimm dir einen Moment Zeit, um dich mit dem Code vertraut zu machen. Folge dazu einfach der Anleitung unten.
Schritt 1: Beispielanwendung ausführen
Nachdem du die TO-DO-App heruntergeladen hast, öffne sie in Android Studio und führe sie aus. Es sollte kompiliert werden. Gehen Sie so vor, um die Anwendung zu entdecken:
- Erstellen Sie eine neue Aufgabe über die Plus-Aktionsschaltfläche. Geben Sie zuerst einen Titel und dann weitere Informationen zur Aufgabe ein. Speichern Sie den Text mit dem grünen Häkchen.
- Klicken Sie in der Liste der Aufgaben auf den Titel der gerade erledigten Aufgabe und sehen Sie sich die Detailseite für die Aufgabe an, um den Rest der Beschreibung aufzurufen.
- Wenn Sie den Status in der Liste oder auf dem Detailbildschirm auf Abgeschlossen setzen möchten, klicken Sie das zugehörige Kästchen an.
- Kehren Sie zum Bildschirm mit den Aufgaben zurück, öffnen Sie das Filtermenü und filtern Sie die Aufgaben nach Aktiv und Erledigt.
- Öffnen Sie die Navigationsleiste und klicken Sie auf Statistiken.
- Kehren Sie zur Übersichtsseite zurück und wählen Sie im Navigationsmenü die Option Alle erledigten Aufgaben löschen aus, um alle Aufgaben mit dem Status Erledigt zu löschen.
Schritt 2: Beispielcode für die App ansehen
Die TO-DO-App basiert auf dem beliebten Test der Architektur und den Architektur-Blueprints, die auf der reaktiven Architektur basieren. Die Anwendung richtet sich nach der Architektur aus dem Leitfaden zur App-Architektur. Dabei werden ViewModels mit Fragmenten, einem Repository und Room verwendet. Wenn Sie mit einem der folgenden Beispiele vertraut sind, sieht die App ähnlich aus:
- Raum mit einem Open Codelab
- Schulungs-Codelabs für Android Kotlin Fundamentals
- Erweiterte Android-Codelabs
- Android-Beispiel für Sonnenblumen
- Android-Apps mit Kotlin-Udacity-Schulungskurs entwickeln
Es ist wichtiger, dass Sie die allgemeine Architektur der App verstehen, als ein besseres Verständnis der Logik auf einer einzelnen Ebene zu haben.
Hier findest du eine Zusammenfassung der Pakete:
Paket: | |
| Aufgabenbildschirm hinzufügen oder bearbeiten:UI-Ebenencode zum Hinzufügen oder Bearbeiten einer Aufgabe. |
| Datenschicht:Dies ist die Datenschicht der Aufgaben. Es enthält den Datenbank-, Netzwerk- und Repository-Code. |
| Statistikbildschirm: UI-Ebenencode für den Statistikbildschirm |
| Bildschirm mit den Aufgabendetails: UI-Ebenencode für eine einzelne Aufgabe. |
| Auf dem Bildschirm „Aufgaben“:Code der UI-Ebene für die Liste aller Aufgaben. |
| Dienstprogramme:Gemeinsame Klassen, die in verschiedenen Teilen der App verwendet werden, z.B. für das Wischen-Layout auf mehreren Bildschirmen. |
Datenschicht (.data)
Diese App enthält eine simulierte Netzwerkebene im Paket remote und eine Datenbankschicht im Paket local. Der Einfachheit halber wird die Netzwerkebene in diesem Projekt mit nur einem HashMap
mit einer Verzögerung simuliert, anstatt echte Netzwerkanfragen zu stellen.
Die DefaultTasksRepository
-Koordinaten oder -Koordinaten zwischen der Netzwerk- und der Datenbankebene sind die Daten, die an die UI-Ebene zurückgegeben werden.
UI-Ebene ( .addedittask, .stats, .taskdetail, .tasks)
Jedes UI-Ebenenpaket enthält ein Fragment und ein Ansichtsmodell sowie alle anderen Klassen, die für die UI erforderlich sind, z. B. einen Adapter für die Aufgabenliste. Die TaskActivity
ist die Aktivität, die alle Fragmente enthält.
Navigation
Die Navigation für die App wird über die Navigationskomponente gesteuert. Die Definition ist in der Datei nav_graph.xml
definiert. Die Navigation wird in den Aufrufmodellen mithilfe der Klasse Event
ausgelöst. Die Aufrufmodelle bestimmen auch, welche Argumente übergeben werden. Die Fragmente beobachten die Event
und führen die tatsächliche Navigation zwischen den Bildschirmen durch.
In diesem Codelab lernen Sie, wie Sie mithilfe von Test-Doubles und der Abhängigkeitsinjektion Repositories, Testmodelle und Fragmente testen. Bevor Sie darauf eingehen, was dies ist, sollten Sie die Begründung dafür kennen, was und wie Sie die Tests erstellen werden.
In diesem Abschnitt werden einige allgemeine Best Practices für Tests behandelt, die für Android gelten.
Die Pyramide
Wenn Sie eine Teststrategie in Betracht ziehen, gibt es drei zugehörige Aspekte:
- Umfang: In welchem Umfang wird der Test ausgelöst? Tests können für eine einzelne Methode, für die gesamte Anwendung oder irgendwo dazwischen durchgeführt werden.
- Geschwindigkeit: Wie schnell wird der Test ausgeführt? Die Testgeschwindigkeit kann von Millisekunden bis zu mehreren Minuten variieren.
- Konformität: Wie hoch ist der Test? Wenn beispielsweise ein Teil des Codes, den Sie testen möchten, eine Netzwerkanfrage senden muss, wird sie dann vom Testcode tatsächlich gesendet oder ist das Ergebnis gefälscht? Wenn der Test tatsächlich mit dem Netzwerk kommuniziert, bedeutet dies einen höheren Grafikqualitätswert. Es gibt aber auch Nachteile.
Solche Aspekte haben Vor- und Nachteile. Beispielsweise sind Geschwindigkeit und Grafikqualität ein Kompromiss. Je schneller du sie testest, desto geringer ist die Grafikqualität und umgekehrt. Eine automatisierte Methode zum Unterteilen automatisierter Tests ist in diese drei Kategorien:
- Einheitentests: Das sind komplexe Tests, die für eine einzelne Klasse ausgeführt werden. Sie werden in der Regel mit einer einzigen Methode durchgeführt. Wenn ein Unit-Test fehlschlägt, können Sie genau wissen, wo in Ihrem Code das Problem auftritt. Da die reale Welt wenig davon bietet, umfasst Ihre App viel mehr als die Ausführung einer Methode oder Klasse. Sie werden schnell genug ausgeführt, sobald Sie Ihren Code ändern. Meist werden lokale Tests durchgeführt (im
test
-Quellsatz). Beispiel: Testen einzelner Methoden in Modellen und Repositories. - Integrationstests: Hiermit wird die Interaktion mehrerer Klassen getestet, um sicherzustellen, dass sie wie erwartet funktionieren, wenn sie gemeinsam verwendet werden. Eine Möglichkeit, Integrationstests zu strukturieren, ist, ein einzelnes Feature zu testen, beispielsweise die Möglichkeit, eine Aufgabe zu speichern. Im Gegensatz zu Unit-Tests wird im Code ein größerer Codebereich ausgewählt, Der Code ist jedoch noch so optimiert, dass er schnell läuft – im Vergleich zu einer Grafikqualität. Sie können je nach Situation entweder lokal oder als Instrumentierungstests ausgeführt werden. Beispiel: Sie testen alle Funktionen eines einzelnen Fragment/Ansicht-Modellpaars.
- End-to-End-Tests (E2e): Sie können eine Kombination von Funktionen testen, die in Kombination ausgeführt werden. Sie testen große Teile der App, simulieren die tatsächliche Nutzung und sind daher in der Regel langsam. Sie bieten die höchste Grafikqualität und zeigen Ihnen, dass Ihre Anwendung insgesamt funktioniert. Diese Tests sind im Wesentlichen instrumentierte Tests (im
androidTest
-Quellsatz).
Beispiel: Sie starten die gesamte App und testen einige Funktionen zusammen.
Der vorgeschlagene Anteil der Tests wird häufig durch eine Pyramide dargestellt, wobei die meisten Tests auf Einheitstests basieren.
Architektur und Tests
Die Möglichkeit, Ihre App auf allen Ebenen der Testpyramide zu testen, ist grundsätzlich mit der App-Architektur verbunden. Bei einer extreme schlecht konzipierten Anwendung könnte beispielsweise die gesamte Logik in einer Methode enthalten sein. Hierzu eignet sich möglicherweise ein End-to-End-Test, da mit diesen Tests normalerweise große Teile der App getestet werden, aber wie wäre es mit Einheiten- oder Integrationstests? 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.
Eine bessere Lösung wäre, die Anwendungslogik in mehrere Methoden und Klassen aufzuteilen, sodass jedes Stück isoliert voneinander getestet werden kann. Mithilfe der Architektur können Sie Ihren Code aufteilen und organisieren. Dadurch lassen sich Tests leichter anhand von Einheiten und Integrationen testen. Die To-do-App, die Sie testen werden, folgt einer bestimmten Architektur:
In dieser Lektion gehen wir darauf ein, wie Sie Teile der obigen Architektur in angemessener Isolierung testen:
- Zuerst testen Sie den Unit-Test auf das Repository.
- Anschließend führen Sie einen Test doppelt im Datenansichtsmodell aus, was für Einheitentests und Integrationstests des Datenansichtsmodells erforderlich ist.
- Als Nächstes lernen Sie, Integrationstests für Fragmente und deren Aufrufmodelle zu schreiben.
- Zum Schluss lernen Sie, wie Sie Integrationstests erstellen, die die Navigationskomponente enthalten.
In der nächsten Lektion werden End-to-End-Tests behandelt.
Wenn Sie einen Unittest für einen Teil einer Klasse (eine Methode oder eine kleine Sammlung von Methoden) schreiben, ist es Ihr Ziel, nur den Code in dieser Klasse zu testen.
Es ist keine leichte Aufgabe, nur Code einer bestimmten Klasse oder von Kursen zu testen. Schauen wir uns dies mal an einem Beispiel an: Öffnen Sie die Klasse data.source.DefaultTaskRepository
im Quellsatz main
. Dies ist das Repository für die App. Für diese Klasse werden Sie als Nächstes Unit-Tests schreiben.
Ihr Ziel ist es, nur den Code in dieser Klasse zu testen. DefaultTaskRepository
ist jedoch erforderlich, damit andere Klassen wie LocalTaskDataSource
und RemoteTaskDataSource
funktionieren. Anders gesagt: LocalTaskDataSource
und RemoteTaskDataSource
sind Abhängigkeiten von DefaultTaskRepository
.
Somit ruft jede Methode in DefaultTaskRepository
Methoden für Datenquellen auf, die wiederum Aufrufmethoden in anderen Klassen verwenden, um Informationen in einer Datenbank zu speichern oder mit dem Netzwerk zu kommunizieren.
Sehen wir uns diese Methode beispielsweise 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 häufigsten Aufrufe in Ihrem Repository. Diese Methode beinhaltet 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 einige konkretere Gründe, warum Tests des Repositorys schwierig sind:
- Sie müssen sich überlegen, eine Datenbank zu erstellen und zu verwalten, um auch die einfachsten Tests für dieses Repository durchführen zu können. Wenn dies der Fall ist, sollte es sich um einen lokalen oder instrumentierten Test handeln. Außerdem sollten Sie AndroidX Test verwenden, um eine simulierte Android-Umgebung zu erhalten.
- Bei manchen Codebereichen, z. B. beim Netzwerkcode, kann es sehr lange dauern, bis sie ausgeführt werden. Manchmal treten sogar Fehler auf, sodass lange, instabile Tests erstellt werden.
- Es kann vorkommen, dass bei Ihren Tests nicht mehr festgestellt werden kann, welcher Code bei einem Testfehler vorliegt. Möglicherweise wird in Ihren Tests Nicht-Repository-Code getestet, sodass beispielsweise der beabsichtigte Repository-Test aufgrund eines Problems mit einem Code des Abhängigkeitens, z. B. des Datenbankcodes, fehlschlagen kann.
Test-Doubles
Die Lösung besteht darin, bei der Prüfung des Repositorys nicht den echten Netzwerk- oder Datenbankcode zu verwenden, sondern stattdessen einen Test doppelt zu verwenden. Ein Test-Double ist eine Version einer Klasse, die speziell zum Testen entwickelt wurde. Er soll die tatsächliche Version einer Klasse in Tests ersetzen. Ähnlich wie bei einem Stunt-Doppelt ist ein Schauspieler, der sich auf Stunts spezialisiert hat, und ersetzt den echten Akteur für gefährliche Handlungen.
Hier einige Arten von Test-Doubles:
Gefälschte | Diese Maßnahme entspricht der Implementierung einer Klasse, doch ist sie so implementiert, dass sie für Tests geeignet ist, aber nicht für die Produktion geeignet ist. |
Mock | Ein Test-Double, das erfasst, welche Methode aufgerufen wurde. Danach wird ein Test bestanden oder abgelehnt, je nachdem, ob die Methoden korrekt aufgerufen wurden. |
Stub | Ein Test-Double, das keine Logik enthält und nur das zurückgibt, was Sie programmieren. Eine |
Dummy | Ein Test-Double, das übergeben, aber nicht verwendet wird, z. B. wenn Sie es nur als Parameter angeben müssen. Wenn Sie einen |
Spion | Mit einem Test-Double werden auch zusätzliche Informationen erfasst. Beispielsweise können Sie für |
Weitere Informationen zu Test-Doubles finden Sie unter Testing to the Toilet: Know Your Test Double.
Die am häufigsten verwendeten Test-Doubles in Android sind Fakes und Mocks.
In dieser Aufgabe erstellen Sie einen FakeDataSource
-Test mit doppeltem Test für die Einheiten DefaultTasksRepository
, der von den tatsächlichen Datenquellen getrennt ist.
Schritt 1: FakeDataSource-Klasse erstellen
In diesem Schritt erstellen Sie die Klasse FakeDataSouce
, die ein doppelter Test von LocalDataSource
und RemoteDataSource
ist.
- Wählen Sie im Test-Quellsatz die Option Neues -> Paket aus.
- Erstellen Sie ein data-Paket mit einem source-Paket.
- Erstellen Sie im Paket data/source eine neue Klasse mit dem Namen
FakeDataSource
.
Schritt 2: TasksDataSource-Schnittstelle implementieren
Sie müssen die anderen Datenquellen ersetzen können, um die neue Klasse „FakeDataSource
“ als Test doppelt verwenden zu können. Diese Datenquellen sind TasksLocalDataSource
und TasksRemoteDataSource
.
- In beiden Fällen wird die
TasksDataSource
-Schnittstelle implementiert.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- Richten Sie
FakeDataSource
ein, umTasksDataSource
zu implementieren:
class FakeDataSource : TasksDataSource {
}
Android Studio beschwert sich, dass du die erforderlichen Methoden für TasksDataSource
nicht implementiert hast.
- Verwenden Sie das Menü zur Schnellkorrektur und wählen Sie Mitglieder implementieren aus.
- Wählen Sie alle Methoden aus und drücken Sie OK.
Schritt 3: Methode „getTasks“ in FakeDataSource implementieren
FakeDataSource
ist eine spezifische Art von Test-Double, das als fake bezeichnet wird. Eine Fälschung ist ein Test-Double, bei dem eine Klasse implementiert ist, sie aber so implementiert ist, dass sie für Tests geeignet ist, aber nicht für die Produktion geeignet ist. „Arbeit“ bedeutet, dass die Klasse bei vorgegebenen Eingaben realistische Ergebnisse liefert.
So wird beispielsweise die gefälschte Datenquelle nicht mit dem Netzwerk verbunden oder in einer Datenbank gespeichert. Stattdessen wird eine Liste im Arbeitsspeicher verwendet. Das funktioniert wie erwartet. Sie erhalten dann bei den Methoden zum Abrufen oder Speichern Aufgaben erwartete Ergebnisse. Sie können diese Implementierung aber nie in der Produktionsumgebung verwenden, da sie nicht auf dem Server oder einer Datenbank gespeichert wird.
Ein FakeDataSource
- ermöglicht es Ihnen, den Code in
DefaultTasksRepository
zu testen, ohne sich auf eine echte Datenbank oder ein echtes Netzwerk verlassen zu müssen. - bietet eine &echte Implementierung für Tests.
- Ändern Sie den
FakeDataSource
-Konstruktor, um einevar
mit dem Namentasks
zu erstellen, die einMutableList<Task>?
mit einem Standardwert einer leeren änderbaren Liste ist.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
Dies ist die Liste der Aufgaben, bei denen es sich um eine Datenbank- oder Serverantwort handelt. Momentan besteht das Ziel darin, die Methode RepositorygetTasks
zu testen. Dadurch werden die Methoden Datenquellen getTasks
, deleteAllTasks
und saveTask
aufgerufen.
Schreiben Sie eine gefälschte Version dieser Methoden:
- „
getTasks
“ schreiben: Wenntasks
„null
“ ist, wird einSuccess
-Ergebnis zurückgegeben. Wenntasks
null
ist, gib einError
-Ergebnis zurück. deleteAllTasks
schreiben: Löscht die änderbare Aufgabenliste.saveTask
schreiben: Die Aufgabe wird der Liste hinzugefügt.
Diese Methoden, die für FakeDataSource
implementiert wurden, sehen wie der unten gezeigte Code 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 findest du die erforderlichen Importanweisungen:
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
Die Funktionsweise entspricht der tatsächlichen lokalen und der Remote-Datenquellen.
In diesem Schritt verwenden Sie die Technik der manuellen Abhängigkeitsinjektion, sodass Sie den soeben erstellten gefälschten Test verwenden können.
Das Hauptproblem ist, dass du ein FakeDataSource
hast, aber es unklar ist, wie du ihn in den Tests verwendest. 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
für diese Klassen erforderlich oder erforderlich ist.
Aktuell werden die Abhängigkeiten in der init
-Methode 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 diese im Wesentlichen hartcodiert. Es ist nicht möglich, den Test doppelt durchzuführen.
Stattdessen sollten Sie die Daten für die Klasse bereitstellen, anstatt sie hartcodieren zu müssen. Das Bereitstellen von Abhängigkeiten wird als Abhängigkeitsabhängigkeit bezeichnet. Es gibt verschiedene Möglichkeiten, Abhängigkeiten bereitzustellen.
Mit der Konstruktorabhängigkeitsinjektion können Sie den Test wiederholen, indem Sie ihn an den Konstruktor übergeben.
Kein Einschleusen | Injektion |
Schritt 1: Konstruktorabhängigkeitseinschleusung in DefaultTasksRepository verwenden
- Ändern Sie den Konstruktor von
DefaultTaskRepository
so, dass sowohl ein Datenquellen (Application
) als auch der Korrespondenz übergeben werden. Das müssen Sie auch für Ihre Tests austauschen. Nähere Informationen dazu finden Sie im dritten Abschnitt zu Koroutinen.
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 Methode
init
. Sie müssen die Abhängigkeiten nicht mehr erstellen. - Löschen Sie auch die alten Instanzvariablen. So verwenden Sie den Konstruktor:
DefaultTasksRepository.kt
// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
- Zuletzt muss die
getRepository
-Methode aktualisiert werden, um den neuen Konstruktor zu verwenden:
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 Konstruktorabhängigkeitsinjektion.
Schritt 2: FakeDataSource in Tests verwenden
Da der Code jetzt die Konstruktorabhängigkeitseinschleusung verwendet, können Sie Ihre DefaultTasksRepository
-Datenquelle testen.
- Klicken Sie mit der rechten Maustaste auf den Kursnamen
DefaultTasksRepository
und wählen Sie Erstellen und dann Testen aus. - Folgen Sie der Anleitung, um
DefaultTasksRepositoryTest
im Quellsatz test zu erstellen. - Fügen Sie oben in der neuen
DefaultTasksRepositoryTest
-Klasse die Mitgliedsvariablen unten ein, um die Daten in den gefälschten Datenquellen zu repräsentieren.
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 Variablen für
FakeDataSource
-Mitglieder (eine für jede Datenquelle in Ihrem Repository) und eine Variable für die zu testendenDefaultTasksRepository
.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Erstellen Sie eine testbare DefaultTasksRepository
und initialisieren Sie sie. Für DefaultTasksRepository
wird der Test doppelt verwendet: FakeDataSource
.
- Erstelle eine Methode namens
createRepository
und vermerke sie mit@Before
. - Instanziieren Sie Ihre falschen Datenquellen mithilfe der Listen
remoteTasks
undlocalTasks
. - Instanziieren Sie Ihren
tasksRepository
mit den beiden soeben erstellten Datenquellen undDispatchers.Unconfined
.
Die letzte Methode sollte dem unten stehenden Code entsprechen.
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: StandardTasksRepository getTasks()-Test schreiben
Zeit, einen DefaultTasksRepository
-Test zu schreiben!
- Schreiben Sie einen Test für die
getTasks
-Methode für das Repository. Prüfen Sie, ob beim Aufrufen vongetTasks
mittrue
, d. h., es sollte aus der Remote-Datenquelle neu geladen werden, es Daten aus der Remote-Datenquelle anstatt der lokalen Datenquelle zurückgibt.
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))
}
Sie erhalten eine Fehlermeldung, wenn Sie getTasks:
anrufen
Schritt 4: runBlockTest hinzufügen
Der Koroutine-Fehler ist zu erwarten, da getTasks
eine suspend
-Funktion ist und Sie eine Koroutine starten müssen, um sie aufzurufen. Dafür benötigen Sie einen Koroutine-Bereich. Um diesen Fehler zu beheben, müssen Sie einige Gradle-Abhängigkeiten hinzufügen, um Coroutinen in Ihren Tests zu verarbeiten.
- Fügen Sie die erforderlichen Abhängigkeiten zum Testen von Koroutinen dem Testquellensatz hinzu. Verwenden Sie dazu
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Synchronisierung nicht vergessen!
kotlinx-coroutines-test
ist die Koroutinentestbibliothek, die speziell zum Testen von Koroutinen entwickelt wurde. Verwenden Sie zum Ausführen Ihrer Tests die Funktion runBlockingTest
. Diese Funktion wird von der Koroutinentestbibliothek bereitgestellt. Dabei wird ein Codeblock verwendet, der dann in einem speziellen Koroutinenkontext ausgeführt wird, der synchron und sofort ausgeführt wird. Die Aktionen werden also in einer deterministischen Reihenfolge ausgeführt. Damit werden Ihre Koroutinen wie Nicht-Coroutinen ausgeführt und sind daher zum Testen von Code gedacht.
Verwende runBlockingTest
in deinen Testklassen, wenn du eine suspend
-Funktion aufrufst. Im nächsten Codelab in dieser Serie erfahren Sie, wie runBlockingTest
funktioniert und wie Sie Koroutinen testen können.
- Fügen Sie
@ExperimentalCoroutinesApi
über dem Kurs hinzu. Das bedeutet, dass Sie wissen, dass Sie eine Koroutine API (runBlockingTest
) in der Klasse verwenden. Andernfalls erhältst du eine Warnung. - Fügen Sie in der Datei
DefaultTasksRepositoryTest
runBlockingTest
hinzu, damit der gesamte Test als Codeblock verwendet wird
Dieser letzte Test sieht aus wie der Code unten.
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 bestätigen Sie, dass er funktioniert und der Fehler behoben ist!
Sie haben gerade gesehen, wie Unittests eines Repositorys ausgeführt werden. In den nächsten Schritten verwenden Sie noch einmal die Einschleusung von Abhängigkeiten und erstellen einen weiteren Testtest. So sehen Sie, wie Sie Unit- und Integrationstests für Ihre Datenansichtsmodelle erstellen.
Unittests sollten nur die Kurse oder Methoden testen, an denen Sie interessiert sind. Dies wird auch als Isolierung bezeichnet, bei der Sie Ihre Einheit eindeutig isolieren und nur den Code testen, der in dieser Einheit enthalten ist.
Daher sollte TasksViewModelTest
nur TasksViewModel
-Code testen – nicht in Datenbank-, Netzwerk- oder Repository-Klassen. Ähnlich wie bei den Repositories, die Sie auch für Ihr Repository gemacht haben, erstellen Sie ein gefälschtes Repository und wenden die Abhängigkeitsinjektion an, um es in den Tests zu verwenden.
In dieser Aufgabe wenden Sie die Abhängigkeitsinjektion an, um Modelle anzusehen.
Schritt 1: TasksRepository-Schnittstelle erstellen
Der erste Schritt bei der Verwendung der Konstruktorabhängigkeitsinjektion ist das Erstellen einer gemeinsamen Schnittstelle, die zwischen der Fake- und der echten Klasse geteilt wird.
Wie sieht das in der Praxis aus? Sehen Sie sich TasksRemoteDataSource
, TasksLocalDataSource
und FakeDataSource
an. Alle haben dieselbe Oberfläche: TasksDataSource
. Damit kannst du im Konstruktor von DefaultTasksRepository
festlegen, dass du ein TasksDataSource
verwendest.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
So können wir den FakeDataSource
ersetzen.
Erstellen Sie als Nächstes eine Schnittstelle für DefaultTasksRepository
, genau wie bei den Datenquellen. Er muss alle öffentlichen Methoden (öffentliche API-Oberfläche) von DefaultTasksRepository
enthalten.
- Öffnen Sie
DefaultTasksRepository
und klicken Sie mit der rechten Maustaste auf den Kursnamen. Wählen Sie dann Refaktorieren -> Extrahieren -> Schnittstelle aus.
- Wählen Sie In separate Datei extrahieren aus.
- Ändern Sie im Fenster Schnittstelle extrahieren den Namen der Schnittstelle in
TasksRepository
. - Klicken Sie im Bereich Mitglieder zur Formularoberfläche auf das Kästchen neben allen Mitgliedern außer den beiden Companion-Mitgliedern und den privaten Methoden.
- Klicken Sie auf Refaktorieren. Die neue
TasksRepository
-Schnittstelle sollte im Paket data/source enthalten sein.
Und DefaultTasksRepository
implementiert nun TasksRepository
.
- Führen Sie die App aus (nicht die Tests), um sicherzugehen, dass alles noch funktioniert.
Schritt 2: FakeTestRepository erstellen
Nachdem Sie die Benutzeroberfläche erstellt haben, können Sie den DefaultTaskRepository
-Test doppelt erstellen.
- Erstellen Sie im test-Dataset in data/source die Kotlin-Datei und die
FakeTestRepository.kt
-Klasse und erweitern Sie dieTasksRepository
-Schnittstelle.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Sie werden dann aufgefordert, die Schnittstellenmethoden zu implementieren.
- Bewegen Sie den Mauszeiger auf den Fehler, bis das Menü mit Vorschlägen angezeigt wird. Klicken Sie dann auf Mitglieder implementieren und wählen Sie sie aus.
- Wählen Sie alle Methoden aus und drücken Sie OK.
Schritt 3: FakeTestRepository-Methoden implementieren
Sie haben jetzt eine FakeTestRepository
-Klasse mit der Methode „nicht implementiert“. Ähnlich wie bei der Implementierung von FakeDataSource
wird FakeTestRepository
auf einer Datenstruktur basiert und nicht wie bei einer komplizierten Vermittlung zwischen lokalen und Remote-Datenquellen behandelt.
Beachte, dass dein FakeTestRepository
FakeDataSource
s oder Ähnliches nicht verwenden muss. Es muss nur realistische gefälschte Ausgaben zurückgeben, die auf Eingaben basieren. Sie verwenden eine LinkedHashMap
, um die Liste der Aufgaben zu speichern, und eine MutableLiveData
für die beobachtbaren Aufgaben.
- Fügen Sie in
FakeTestRepository
sowohl die VariableLinkedHashMap
für die aktuelle Aufgabenliste als auch eineMutableLiveData
für die beobachtbaren Aufgaben ein.
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 solltetasksServiceData
verwenden und in eine Liste mittasksServiceData.values.toList()
umwandeln und dann alsSuccess
-Ergebnis zurückgeben.refreshTasks
: Aktualisiert den Wert vonobservableTasks
ingetTasks()
.observeTasks
: Erstellt eine Koroutine mitrunBlocking
und führtrefreshTasks
aus. Gibt dannobservableTasks
zurück.
Im Folgenden sehen 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 sollte man einige Tasks
bereits in Ihrem Repository haben. Sie könnten saveTask
mehrmals aufrufen. Dies lässt sich vereinfachen, wenn Sie eine spezielle Methode für Tests hinzufügen, mit der Aufgaben hinzugefügt werden können.
- Fügen Sie die Methode
addTasks
hinzu, bei der einvararg
von Aufgaben verarbeitet wird. Jede Methode wird in dieHashMap
aufgenommen und die Aufgaben werden dann aktualisiert.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
An dieser Stelle haben Sie ein gefälschtes Repository für Tests mit einigen der wichtigsten Methoden. Verwenden Sie dies anschließend in Ihren Tests.
Bei dieser Aufgabe verwenden Sie eine gefälschte Klasse innerhalb eines ViewModel
. Verwenden Sie die Konstruktorabhängigkeitseinschleusung, um die beiden Datenquellen über die Konstruktorabhängigkeitsinjektion aufzunehmen. Fügen Sie dazu dem Konstruktor TasksViewModel
&s> eine TasksRepository
-Variable hinzu.
Bei Betrachtungsmodellen ist das etwas anders, da sie nicht direkt erstellt werden. Beispiel:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Wie im Code oben, verwenden Sie den viewModel's
Property-Bevollmächtigten, der das Ansichtsmodell erstellt. Um das Aufbauen des Ansichtsmodells zu ändern, müssen Sie ein ViewModelProvider.Factory
hinzufügen und verwenden. Wenn du ViewModelProvider.Factory
noch nicht kennst, findest du hier weitere Informationen dazu.
Schritt 1: ViewModelFactory in TasksViewModel erstellen und verwenden
Sie beginnen mit der Aktualisierung der Kurse und dem Test zum Bildschirm Tasks
.
- Öffnen Sie
TasksViewModel
. - Ändern Sie den Konstruktor von
TasksViewModel
so, dassTasksRepository
verwendet wird, anstatt ihn innerhalb 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. Speichern Sie die Factory-Klasse in die Datei wie die TasksViewModel
, aber Sie können auch eine eigene Datei dafür verwenden.
- Fügen Sie unten in der
TasksViewModel
-Datei außerhalb des Kurses einTasksViewModelFactory
-Element ein, das ein einfachesTasksRepository
-Element benötigt.
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 standardmäßig, wie ViewModel
s konstruiert werden. Nachdem Sie die Werksansicht erstellt haben, können Sie sie überall dort verwenden, wo Sie das Datenansichtsmodell erstellen.
- Aktualisieren Sie
TasksFragment
, um die Werkseinstellungen 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 App-Code aus und prüfen Sie, ob alles funktioniert.
Schritt 2: FakeTestRepository in TasksViewModelTest verwenden
Anstatt das echte Repository für Ihre Modellmodelle zu verwenden, können Sie das gefälschte Repository nutzen.
- Öffnen Sie
TasksViewModelTest
. - Füge in
TasksViewModelTest
eine PropertyFakeTestRepository
hinzu.
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
setupViewModel
-Methode, um eineFakeTestRepository
mit drei Aufgaben zu erstellen, und erstellen Sie dann dietasksViewModel
mit 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 du den AndroidX-Test
ApplicationProvider.getApplicationContext
nicht mehr verwendest, kannst du die@RunWith(AndroidJUnit4::class)
-Annotation auch entfernen. - Führen Sie die Tests durch und sorgen Sie dafür, dass sie weiterhin funktionieren.
Mithilfe der Konstruktorabhängigkeitseinschleusung haben Sie DefaultTasksRepository
jetzt als Abhängigkeit entfernt und in den Tests durch FakeTestRepository
ersetzt.
Schritt 3: TaskDetail-Fragment und ViewModel ebenfalls aktualisieren
Nehmen Sie die gleichen Änderungen für TaskDetailFragment
und TaskDetailViewModel
vor. Dadurch wird der Code für den nächsten TaskDetail
-Test vorbereitet.
- Öffnen Sie
TaskDetailViewModel
. - 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
TaskDetailViewModel
-Datei außerhalb des Kurses einTaskDetailViewModelFactory
-Element ein.
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)
}
- Aktualisieren Sie
TasksFragment
, um die Werkseinstellungen 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 den Code aus und prüfen Sie, ob alles in Ordnung ist.
Anstatt des echten Repositorys in TasksFragment
und TasksDetailFragment
können Sie jetzt FakeTestRepository
verwenden.
Als Nächstes schreiben Sie Integrationstests, um Ihre Fragment- und Aufrufmodellinteraktionen zu testen. Sie können herausfinden, ob der Modellcode Ihrer Ansicht Ihre UI entsprechend aktualisiert. Dazu verwenden Sie
- „ServiceLocator“-Muster
- die Espressomaschinen und Mockito-Bibliotheken
Integrationstests testen die Interaktion mehrerer Klassen, um dafür zu sorgen, dass sie wie erwartet funktionieren. Diese Tests können entweder lokal (test
Quellsatz) oder als Instrumentierungstests (androidTest
Quellensatz) ausgeführt werden.
In deinem Fall nimmst du jedes Fragment mit und schreibst Integrationstests für das Fragment und das Modell, um die Hauptmerkmale 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 für das Schreiben grundlegender Testanweisungen erforderlich ist.androidx.test:core
: AndroidX-Testbibliothekkotlinx-coroutines-test
: Die Koroutinentestbibliothekandroidx.fragment:fragment-testing
: AndroidX-Testbibliothek zum Erstellen von Fragmenten in Tests und zum Ändern ihres Status.
Da Sie diese Bibliotheken im androidTest
-Quellsatz verwenden, können Sie sie mit androidTestImplementation
als Abhängigkeiten hinzufügen.
Schritt 2: TaskDetailFragmentTest-Klasse erstellen
Das TaskDetailFragment
zeigt Informationen zu einer einzelnen Aufgabe an.
Schreibe zuerst einen Fragmenttest für TaskDetailFragment
, da die Funktion im Vergleich zu den anderen Fragmenten recht einfach ist.
- Öffnen Sie
taskdetail.TaskDetailFragment
. - Erstelle einen Test für
TaskDetailFragment
, wie du es bereits getan hast. Übernehmen Sie die Standardauswahl und fügen Sie sie in den androidTest-Quellensatz ein (nicht dentest
-Quellsatz).
- Fügen Sie der Klasse
TaskDetailFragmentTest
die folgenden Annotationen hinzu.
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
Ziel dieser Anmerkung ist:
@MediumTest
: Der Test wird als Integrationstest für mittlere Laufzeiten (im Vergleich zu@SmallTest
-Einheitentests und@LargeTest
-Großen End-to-End-Tests) gekennzeichnet. Damit können Sie den Test besser gruppieren und die Testgröße auswählen.@RunWith(AndroidJUnit4::class)
: Wird in jedem Kurs verwendet, der den AndroidX-Test verwendet.
Schritt 3: Fragment aus einem Test starten
In dieser Aufgabe starten Sie TaskDetailFragment
mithilfe der AndroidX-Testbibliothek. FragmentScenario
ist eine Klasse aus AndroidX Test, die das Fragment umschließt und Ihnen direkte Kontrolle über den Lebenszyklus des Fragments zu Testzwecken bietet. Zum Erstellen von Tests für Fragmente erstellst du eine FragmentScenario
für das Fragment, das du testest (TaskDetailFragment
).
- Kopieren Sie diesen Test in die Spalte
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 einen
Bundle
, der die Fragmentargumente für die Aufgabe darstellt, die an das Fragment übergeben werden. - Mit der Funktion
launchFragmentInContainer
wird einFragmentScenario
mit diesem Set und einem Design erstellt.
Dieser Test ist noch nicht abgeschlossen, da er noch nichts beansprucht. Führen Sie vorerst den Test durch und beobachten Sie, was passiert.
- Dies ist ein Instrumentierungstest. Prüfen Sie daher, ob der Emulator oder Ihr Gerät sichtbar ist.
- Führen Sie den Test aus.
Es sollten einige Dinge passieren.
- Da dies ein Instrumentierungstest ist, wird der Test entweder auf Ihrem physischen Gerät (falls verbunden) oder in einem Emulator durchgeführt.
- Das Fragment sollte gestartet werden.
- Sie sieht, dass er nicht durch ein anderes Fragment navigiert oder Menüs enthält, die mit der Aktivität verknüpft sind – es ist nur das Fragment.
Sehen Sie sich dann genau an und bemerken Sie, dass das Fragment „& data“ nicht anzeigt, weil die Aufgabendaten nicht geladen werden können.
In deinem Test müssen die TaskDetailFragment
geladen werden (die du durchgeführt hast) und bestätigen, dass die Daten richtig geladen wurden. Warum gibt es keine Daten? Der Grund hierfür ist, 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)
}
Diese Datei enthält die Datei FakeTestRepository
, aber du musst dein tatsächliches Repository durch ein gefälschtes für dein Fragment ersetzen. Nächste Schritte!
Bei dieser Aufgabe stellst du das gefälschte Repository mit einem ServiceLocator
in deinem Fragment bereit. So kannst du dein Fragment schreiben und Tests zur Modellintegration aufrufen.
Sie können hier keine Konstruktorabhängigkeitseinschleusung verwenden, wie zuvor, als Sie eine Abhängigkeit vom Ansichtsmodell oder Repository angeben mussten. Bei der Einbindung der Konstruktorabhängigkeit müssen Sie die Klasse konstruieren. Fragmente und Aktivitäten sind Beispiele für Klassen, die Sie erstellen und im Allgemeinen auf den Konstruktor zugreifen können.
Da du das Fragment nicht konstruierst, kannst du die Konstruktorabhängigkeitseinschleusung verwenden, um den Repository-Test doppelt (FakeTestRepository
) gegen das Fragment zu ändern. Verwenden Sie stattdessen das Muster Dienstsuche. Das Service Locator-Muster ist eine Alternative zur Abhängigkeitsinjektion. Dabei wird eine Singleton-Klasse namens „Service Locator“ erstellt, mit der Abhängigkeiten sowohl für den normalen als auch für den Testcode bereitgestellt werden. Im normalen App-Code (der main
-Quellsatz) sind alle diese Abhängigkeiten die regulären App-Abhängigkeiten. Für die Tests modifizieren Sie die Dienstsuche, um doppelte Versionen der Abhängigkeiten bereitzustellen.
Keine Verwendung der Dienstsuche | Dienstsuche verwenden |
Für diese Codelab-App gehen Sie so vor:
- Erstellen Sie eine Service Locator-Klasse, die ein Repository erstellen und speichern kann. Standardmäßig wird ein „&normal“-Repository erstellt.
- Refaktorieren Sie Ihren Code. Verwenden Sie dazu die Dienstsuche.
- Rufen Sie in Ihrer Testklasse eine Methode für die Dienstsuche auf, bei der das Repository „normal“ durch Ihren doppelten Datensatz ersetzt wird.
Schritt 1: ServiceLocator erstellen
Lass ServiceLocator
einen Kurs erstellen. Er befindet sich in der Hauptquelle, die mit dem Rest des App-Codes festgelegt ist, da der Code vom Hauptanwendungscode verwendet wird.
Hinweis: ServiceLocator
ist ein Singleton-Element. Verwenden Sie daher für die Klasse das Kotlin-Keyword object
.
- Erstellen Sie die Datei ServiceLocator.kt auf der obersten Ebene des Hauptquellsatzes.
- Definieren Sie einen
object
mit dem NamenServiceLocator
. - Erstellen Sie die Instanzvariablen
database
undrepository
und setzen Sie beide aufnull
. - Annotieren Sie das Repository
@Volatile
, da es von mehreren Threads verwendet werden kann (@Volatile
ausführliche Beschreibung hier).
Der Code sollte wie unten dargestellt aussehen.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Aktuell musst du bei ServiceLocator
nur noch wissen, wie TasksRepository
zurückgegeben wird. Sie gibt eine vorhandene DefaultTasksRepository
zurück oder erstellt bei Bedarf eine neue DefaultTasksRepository
.
Definieren Sie die folgenden Funktionen:
provideTasksRepository
: Entweder stellt er ein bereits vorhandenes Repository bereit oder erstellt ein neues. Diese Methode sollte aufthis
synchronized
lauten. In Situationen mit mehreren Threads wird nie versehentlich zwei Repository-Instanzen erstellt.createTasksRepository
: Code zum Erstellen eines neuen Repositorys. RufecreateTaskLocalDataSource
auf und erstellt ein neuesTasksRemoteDataSource
.createTaskLocalDataSource
: Code zum Erstellen einer neuen lokalen Datenquelle WirdcreateDataBase
angerufen.createDataBase
: Code zum Erstellen einer neuen Datenbank
Der vollständige Code finden Sie unten.
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 ändern den Hauptanwendungscode (nicht die Tests) so, dass das Repository an einem Ort, in Ihrem ServiceLocator
, erstellt wird.
Es ist wichtig, dass Sie immer nur eine Instanz der Repository-Klasse erstellen. Deshalb verwenden Sie die Dienstsuche in meiner Anwendungsklasse.
- Öffnen Sie auf der obersten Ebene der Pakethierarchie
TodoApplication
und erstellen Sie eineval
für Ihr Repository. Weisen Sie diesem dann ein Repository zu, das Sie mitServiceLocator.provideTaskRepository
abgerufen haben.
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 in der Anwendung ein Repository erstellt haben, können Sie die alte getRepository
-Methode in DefaultTasksRepository
entfernen.
- Öffnen Sie
DefaultTasksRepository
und 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
}
}
}
}
Verwende jetzt überall, wo du getRepository
verwendet hast, stattdessen die Anwendung taskRepository
. So wird sichergestellt, dass Sie statt des Repositorys direkt das Repository ServiceLocator
erhalten.
- Öffnen Sie
TaskDetailFragement
und suchen Sie oben im Kurs den Anruf beigetRepository
. - Ersetzen Sie diesen Aufruf durch einen Aufruf, der das Repository von
TodoApplication
erhält.
TaskDetailFragment.kt (in englischer Sprache)
// 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 den Code, der das Repository erwirbt, für
StatisticsViewModel
undAddEditTaskViewModel
, damit es aus demTodoApplication
verwendet wird.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Führe deine Anwendung aus (nicht den Test).
Da Sie nur refaktoriert haben, sollte die Anwendung ohne Probleme ausgeführt werden.
Schritt 3: FakeAndroidTestRepository erstellen
Sie haben bereits eine FakeTestRepository
in der Testquelle festgelegt. Standardmäßig können Testklassen nicht zwischen den Quellsätzen test
und androidTest
freigegeben werden. Sie müssen also eine duplizierte FakeTestRepository
-Klasse im androidTest
-Quellsatz erstellen und sie FakeAndroidTestRepository
nennen.
- Klicken Sie mit der rechten Maustaste auf die
androidTest
-Quellgruppe und erstellen Sie ein data-Paket. Klicken Sie mit der rechten Maustaste noch einmal und erstellen Sie ein Quellpaket. - Erstellen Sie in diesem Quellpaket eine neue Klasse mit dem Namen
FakeAndroidTestRepository.kt
. - Kopieren Sie folgenden Code in die 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
Ok, Zeit, die ServiceLocator
zu verwenden, um den Test zu verdoppeln. Fügen Sie dazu Ihrem ServiceLocator
-Code Code hinzu.
- Öffnen Sie
ServiceLocator.kt
. - Markieren Sie den Setter für
tasksRepository
als@VisibleForTesting
. Mit dieser Annotation möchten wir klarstellen, dass der Setter auf Tests zurückzuführen ist.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Unabhängig davon, ob Sie einen Test allein oder in einer Gruppe von Tests durchführen, sollten die Tests genau gleich sein. Das bedeutet, dass Ihre Tests kein Verhalten annehmen sollten, das voneinander abhängt, d. h., dass keine Objekte zwischen Tests geteilt werden.
Da es sich bei der ServiceLocator
um einen Singleton handelt, kann es passieren, dass sie versehentlich zwischen Tests geteilt wird. Das lässt sich vermeiden, indem du eine Methode erstellst, mit der der Status ServiceLocator
zwischen Tests korrekt zurückgesetzt wird.
- Fügen Sie eine Instanzvariable namens
lock
mit dem WertAny
hinzu.
ServiceLocator.kt
private val lock = Any()
- Fügen Sie eine testspezifische Methode namens
resetRepository
hinzu, mit der die Datenbank gelöscht und sowohl das Repository als auch die Datenbank auf null gesetzt werden.
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 den ServiceLocator
.
- Öffnen Sie
TaskDetailFragmentTest
. - Deklarieren Sie eine
lateinit TasksRepository
-Variable. - Fügen Sie eine Einrichtung und eine Bereinigungsmethode hinzu, um eine
FakeAndroidTestRepository
vor jedem Test einzurichten.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Sie müssen den Funktionstext von
activeTaskDetails_DisplayedInUi()
inrunBlockingTest
umschließen. - Speichere
activeTask
im Repository, bevor du das Fragment startest.
repository.saveTask(activeTask)
Der letzte Test sieht aus wie unten angegeben.
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)
}
- Annotieren Sie den gesamten Kurs 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ühre den
activeTaskDetails_DisplayedInUi()
-Test aus.
Ähnlich wie zuvor sollten Sie das Fragment sehen. Nur dass Sie dieses Mal das Repository richtig einrichten, sehen Sie jetzt die Aufgabeninformationen.
In diesem Schritt führen Sie den ersten Integrationstest durch die Espresso-UI-Testbibliothek durch. Sie haben Ihren Code so strukturiert, dass Sie Tests mit Assertions für Ihre Benutzeroberfläche hinzufügen können. Verwenden Sie dazu die Espresso-Testbibliothek.
Espresso hilft Ihnen:
- Mit Ansichten interagieren, z. B. Schaltflächen klicken, eine Leiste verschieben oder nach unten scrollen
- Festlegen, dass bestimmte Ansichten auf dem Bildschirm angezeigt werden oder sich in einem bestimmten Zustand befinden (z. B. bestimmter Text oder ein Kästchen)
Schritt 1: Gradle-Abhängigkeit beachten
Die Hauptabhängigkeit von Espresso ist bereits vorhanden, weil sie standardmäßig in Android-Projekten enthalten ist.
app/build.gradle
dependencies {
// ALREADY in your code
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
// Other dependencies
}
androidx.test.espresso:espresso-core
: Diese Espresso-Hauptabhängigkeit ist standardmäßig enthalten, wenn Sie ein neues Android-Projekt erstellen. Sie enthält den grundlegenden Testcode für die meisten Aufrufe und Aktionen.
Schritt 2: Animationen deaktivieren
Espressotests werden auf einem echten Gerät ausgeführt und sind somit gewisse Instrumentierungstests. Ein Problem tritt auf, wenn Animationen animiert und versuchen, zu testen, ob ein Aufruf auf dem Bildschirm angezeigt wird. Wenn die Animation aber weiterhin animiert wird, kann der Espresso-Nutzer versehentlich einen Test durchführen. Dadurch sind Espresso-Tests instabil.
Für Espresso-UI-Tests wird empfohlen, Animationen zu deaktivieren (sowie Ihr Test schneller ausgeführt wird):
- Gehen Sie auf Ihrem Testgerät zu Einstellungen > Entwickleroptionen.
- Deaktivieren Sie diese drei Einstellungen: Skalierung der Fensteranimation, Übergangsanimation und Animation mit Dauer.
Schritt 3: Espressotest ansehen
Sehen Sie sich einen Espresso-Code an, bevor Sie einen Espresso-Test schreiben.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
Diese Anweisung enthält die Kästchenansicht mit der ID task_detail_complete_checkbox
, klickt darauf und bestätigt, dass das Kästchen angeklickt ist.
Die meisten Espresso-Berichte bestehen aus vier Teilen:
onView
onView
ist ein Beispiel für eine statische Espresso-Methode, mit der eine Espresso-Anweisung gestartet wird. onView
ist eine der gängigsten. Es gibt jedoch auch andere Optionen, z. B. onData
.
2. ViewMatcher
withId(R.id.task_detail_title_text)
withId
ist ein Beispiel für einen ViewMatcher
, der eine Ansicht nach ihrer ID abruft. Es gibt weitere Viewer-Matcher, die Sie in der Dokumentation nachschlagen können.
3. ViewAction,
perform(click())
Die Methode perform
, die eine ViewAction
verwendet. Ein ViewAction
kann in der Ansicht ausgeführt werden, z. B. kann es auf die Ansicht klicken.
4. ViewAssertion (in englischer Sprache)
check(matches(isChecked()))
check
, die ViewAssertion
verwenden. ViewAssertion
überprüft oder vertritt die Ansicht. Die am häufigsten verwendete ViewAssertion
-Regel ist die Assertion matches
. Um die Assertion zu beenden, verwenden Sie eine andere ViewMatcher
, in diesem Fall isChecked
.
Beachten Sie, dass Sie in einer Espresso-Anweisung nicht immer sowohl perform
als auch check
aufrufen. Du kannst Aussagen verwenden, die einfach eine Assertion mit check
erstellen oder einfach ViewAction
mit perform
erstellen.
- Öffnen Sie
TaskDetailFragmentTest.kt
. - Aktualisiere 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 findest du die erforderlichen Importanweisungen:
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 Kommentar von
// THEN
verwendet Espresso. Prüfe die Teststruktur und die Verwendung vonwithId
und prüfe, ob die Detailseite korrekt ist. - Führen Sie den Test aus und prüfen Sie, ob er bestanden wurde.
Schritt 4: Optional: eigenen Espressotest schreiben
Testen Sie nun selbst.
- Erstellen Sie einen neuen Test mit dem Namen
completedTaskDetails_DisplayedInUi
und 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 ihn ab.
- Ausführen und bestätigen Sie den Test.
Der 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 Schritt lernen Sie, wie Sie die Navigationskomponente und die Testbibliothek Mockito mit einem anderen Testtyp namens Simulation testen.
In diesem Codelab haben Sie ein Test-Double verwendet, das als Fake bezeichnet wird. Fälschungen sind eine von vielen Arten von Test-Doppelten. Welchen Testpfad sollten Sie zum Testen der Navigationskomponente verwenden?
Überlege, wie die Navigation funktioniert. Stellen Sie sich vor, Sie klicken auf eine der Aufgaben in TasksFragment
, um zu einem Aufgabendetailbildschirm zu wechseln.
Code hier in TasksFragment
, der zu einem Aufgabendetailbildschirm wechselt, wenn er gedrückt wird.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
Die Navigation erfolgt aufgrund eines Aufrufs der navigate
-Methode. Wenn du eine Zusicherungserklärung schreiben musst, gibt es eine einfache Möglichkeit zu testen, ob du zu TaskDetailFragment
navigiert hast. Die Navigation ist kompliziert und führt über die Initialisierung von TaskDetailFragment
hinaus nicht zu einer deutlichen Ausgabe oder Statusänderung.
Sie können bestätigen, dass die Methode navigate
mit dem richtigen Aktionsparameter aufgerufen wurde. Und genau das funktioniert mit einem Mock-Test. Damit wird ermittelt, ob bestimmte Methoden aufgerufen wurden.
Mockito ist ein Framework für Test-Doubles. Während das Wort „Simulation“ in der API und im Namen verwendet wird, dient es nicht nur dafür, Simulationen durchzuführen. Es kann auch Stuben und Geister zaubern.
Sie verwenden Mockito, um eine Simulation NavigationController
zu erstellen, mit der Sie bestätigen können, dass die Navigationsmethode 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
: Das 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. Auf Android-Geräten erfolgt dies mit Dekox-Code. Daher ermöglicht diese Bibliothek Mockito, Objekte während der Laufzeit von Android zu generieren.androidx.test.espresso:espresso-contrib
: Diese Bibliothek besteht aus externen Beiträgen (also dem Namen), die Testcode für komplexere Ansichten wieDatePicker
undRecyclerView
enthalten. Außerdem finden Sie hier Bedienungshilfen-Prüfungen und die Klasse „CountingIdlingResource
“, die später behandelt wird.
Schritt 2: Tasks-FragmentTest erstellen
- Öffnen Sie
TasksFragment
. - Klicken Sie mit der rechten Maustaste auf den Kursnamen
TasksFragment
und wählen Sie Erstellen und dann Testen aus. Erstellen Sie einen Test im Quell-Dataset androidTest. - Kopieren Sie diesen Code in den
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 von Ihnen geschriebenen Code: TaskDetailFragmentTest
. Es wird eingerichtet und reißt ein FakeAndroidTestRepository
. Mit einem Navigationstest können Sie testen, ob Sie beim Klicken auf eine Aufgabe in der Aufgabenliste zur richtigen TaskDetailFragment
gelangen.
- Fügen Sie den Test
clickTask_navigateToDetailFragmentOne
hinzu.
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)
}
- Die Mockito-Funktion
mock
dient zum Erstellen einer Simulation.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Wenn Sie Simulationen in Mockito durchführen möchten, müssen Sie den Kurs übergeben, den Sie simulieren möchten.
Als Nächstes musst du den NavController
mit dem Fragment verknüpfen. Mit onFragment
kannst du Methoden für das Fragment selbst aufrufen.
- Macht das neue Modell zu dem Fragment
NavController
.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Fügen Sie den Code hinzu, um mit
RecyclerView
auf das Element mit dem Text „TITLE1“ zu klicken.
// 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 Ihnen, Espresso-Aktionen in einer RecyclerView auszuführen.
- Prüfen Sie, ob
navigate
mit 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
ist der Grund dafür. Du kannst die Mock-Methode navController
, die als eine bestimmte Methode (navigate
) bezeichnet wird, mit einem Parameter (actionTasksFragmentToTaskDetailFragment
mit der ID &&t1; bestätigen.)
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")
)
}
- Testen Sie es.
Sie haben folgende Möglichkeiten, um die Navigation zu testen:
- Mit Mockito wird ein
NavController
-Mock erstellt. - Hängen Sie den simulierten
NavController
an das Fragment an. - Prüfen Sie, ob die Navigation mit der richtigen Aktion und den richtigen Parametern aufgerufen wurde.
Schritt 3: Optional: Schreiben Sie „ClickAddTaskButton_navigationToAddEditFragment“
Testen Sie, ob Sie einen Navigationstest selbst schreiben können.
- Schreiben Sie den Test
clickAddTaskButton_navigateToAddEditFragment
. Dabei wird geprüft, ob dieAddEditTaskFragment
aufgerufen wird, wenn Sie auf den + FAB klicken.
Die Antwort findest du 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 den Unterschied zwischen dem eingegebenen Code und dem endgültigen Code zu sehen.
Mit dem folgenden Git-Befehl können Sie den Code des fertigen Codelabs herunterladen:
$ 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, entpacken und in Android Studio öffnen.
In diesem Codelab erfährst du, wie du eine manuelle Abhängigkeitsinjektion, eine Dienstsuche und die Verwendung von Fälschungen und Simulationen in deinen Android-Kotlin-Apps einrichtest. Wichtig ist insbesondere:
- Was Sie testen möchten und Ihre Teststrategie bestimmt, welche Arten von Tests Sie für Ihre App implementieren. Einheitentests sind fokussiert und schnell. Integrationstests prüfen die Interaktion zwischen Teilen deines Programms. Ende-zu-Ende-Tests: Sie verifizieren Features, haben die höchste Genauigkeit, sind häufig instrumentiert und können länger dauern.
- Die Architektur Ihrer App beeinflusst, wie schwierig das Testen ist.
- Für die Strategie „TDD oder Test Driven Development“ werden die Tests zuerst geschrieben, dann wird die Funktion erstellt, um sie zu bestehen.
- Mit Test-Doubles können Sie Teile Ihrer App zu Testzwecken isolieren. Ein Test-Double ist eine Version einer Klasse, die speziell zum Testen entwickelt wurde. zum Beispiel, wenn Sie Daten aus einer Datenbank oder dem Internet abrufen.
- Verwenden Sie die Abhängigkeitsabhängigkeit, um eine echte Klasse durch eine Testklasse wie ein Repository oder eine Netzwerkschicht zu ersetzen.
- Verwenden Sie erweiterte Tests (
androidTest
), um UI-Komponenten zu starten. - Wenn du eine Konstruktorabhängigkeitseinschleusung verwenden kannst, um beispielsweise ein Fragment zu starten, kannst du oft eine Dienstsuche verwenden. Das Muster für die Filialsuche ist eine Alternative zur Abhängigkeitsinjektion. Dabei wird eine Singleton-Klasse namens „Service Locator“ erstellt, mit der Abhängigkeiten sowohl für den normalen als auch für den Testcode bereitgestellt werden.
Udacity-Kurs:
Android-Entwicklerdokumentation:
- Leitfaden zur App-Architektur
runBlocking
undrunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- Android X-Testbibliothek
- Core-Bibliothek für AndroidX-Architekturkomponenten
- Quellsätze
- Über die Befehlszeile testen
Videos:
Sonstiges:
Links zu weiteren Codelabs in diesem Kurs finden Sie auf der Landingpage des erweiterten Android-Tools in Kotlin.