Einführung in das Testen von doppelten Werten und der Einschleusung von Abhängigkeiten

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:

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:

Zip herunterladen

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:

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: com.example.android.architecture.blueprints.todoapp

.addedittask

Aufgabenbildschirm hinzufügen oder bearbeiten:UI-Ebenencode zum Hinzufügen oder Bearbeiten einer Aufgabe.

.data

Datenschicht:Dies ist die Datenschicht der Aufgaben. Es enthält den Datenbank-, Netzwerk- und Repository-Code.

.statistics

Statistikbildschirm: UI-Ebenencode für den Statistikbildschirm

.taskdetail

Bildschirm mit den Aufgabendetails: UI-Ebenencode für eine einzelne Aufgabe.

.tasks

Auf dem Bildschirm „Aufgaben“:Code der UI-Ebene für die Liste aller Aufgaben.

.util

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:

  1. Zuerst testen Sie den Unit-Test auf das Repository.
  2. Anschließend führen Sie einen Test doppelt im Datenansichtsmodell aus, was für Einheitentests und Integrationstests des Datenansichtsmodells erforderlich ist.
  3. Als Nächstes lernen Sie, Integrationstests für Fragmente und deren Aufrufmodelle zu schreiben.
  4. 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 StubTaskRepository kann beispielsweise so programmiert werden, dass sie bestimmte Kombinationen von Aufgaben aus getTasks zurückgibt.

Dummy

Ein Test-Double, das übergeben, aber nicht verwendet wird, z. B. wenn Sie es nur als Parameter angeben müssen. Wenn Sie einen NoOpTaskRepository haben, wird der TaskRepository einfach mit kein Code in jeder der Methoden implementiert.

Spion

Mit einem Test-Double werden auch zusätzliche Informationen erfasst. Beispielsweise können Sie für SpyTaskRepository erfassen, wie oft die Methode addTask aufgerufen wurde.

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.

  1. Wählen Sie im Test-Quellsatz die Option Neues -> Paket aus.

  1. Erstellen Sie ein data-Paket mit einem source-Paket.
  2. 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.

  1. 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 { ... }
  1. Richten Sie FakeDataSource ein, um TasksDataSource zu implementieren:
class FakeDataSource : TasksDataSource {

}

Android Studio beschwert sich, dass du die erforderlichen Methoden für TasksDataSource nicht implementiert hast.

  1. Verwenden Sie das Menü zur Schnellkorrektur und wählen Sie Mitglieder implementieren aus.


  1. 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.
  1. Ändern Sie den FakeDataSource-Konstruktor, um eine var mit dem Namen tasks zu erstellen, die ein MutableList<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:

  1. getTasks“ schreiben: Wenn tasksnull“ ist, wird ein Success-Ergebnis zurückgegeben. Wenn tasks null ist, gib ein Error-Ergebnis zurück.
  2. deleteAllTasks schreiben: Löscht die änderbare Aufgabenliste.
  3. 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

  1. Ä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 }
  1. Da Sie die Abhängigkeiten übergeben haben, entfernen Sie die Methode init. Sie müssen die Abhängigkeiten nicht mehr erstellen.
  2. 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
  1. 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.

  1. Klicken Sie mit der rechten Maustaste auf den Kursnamen DefaultTasksRepository und wählen Sie Erstellen und dann Testen aus.
  2. Folgen Sie der Anleitung, um DefaultTasksRepositoryTest im Quellsatz test zu erstellen.
  3. 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 }
  1. 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 testenden DefaultTasksRepository.

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.

  1. Erstelle eine Methode namens createRepository und vermerke sie mit @Before.
  2. Instanziieren Sie Ihre falschen Datenquellen mithilfe der Listen remoteTasks und localTasks.
  3. Instanziieren Sie Ihren tasksRepository mit den beiden soeben erstellten Datenquellen und Dispatchers.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!

  1. Schreiben Sie einen Test für die getTasks-Methode für das Repository. Prüfen Sie, ob beim Aufrufen von getTasks mit true, 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.

  1. 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.

  1. 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.
  2. 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))
    }

}
  1. 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.

  1. Öffnen Sie DefaultTasksRepository und klicken Sie mit der rechten Maustaste auf den Kursnamen. Wählen Sie dann Refaktorieren -> Extrahieren -> Schnittstelle aus.

  1. Wählen Sie In separate Datei extrahieren aus.

  1. Ändern Sie im Fenster Schnittstelle extrahieren den Namen der Schnittstelle in TasksRepository.
  2. 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.


  1. Klicken Sie auf Refaktorieren. Die neue TasksRepository-Schnittstelle sollte im Paket data/source enthalten sein.

Und DefaultTasksRepository implementiert nun TasksRepository.

  1. 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.

  1. Erstellen Sie im test-Dataset in data/source die Kotlin-Datei und die FakeTestRepository.kt-Klasse und erweitern Sie die TasksRepository-Schnittstelle.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Sie werden dann aufgefordert, die Schnittstellenmethoden zu implementieren.

  1. 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.
  1. 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 FakeDataSources 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.

  1. Fügen Sie in FakeTestRepository sowohl die Variable LinkedHashMap für die aktuelle Aufgabenliste als auch eine MutableLiveData 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:

  1. getTasks: Diese Methode sollte tasksServiceData verwenden und in eine Liste mit tasksServiceData.values.toList() umwandeln und dann als Success-Ergebnis zurückgeben.
  2. refreshTasks: Aktualisiert den Wert von observableTasks in getTasks().
  3. observeTasks: Erstellt eine Koroutine mit runBlocking und führt refreshTasks aus. Gibt dann observableTasks 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.

  1. Fügen Sie die Methode addTasks hinzu, bei der ein vararg von Aufgaben verarbeitet wird. Jede Methode wird in die HashMap 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.

  1. Öffnen Sie TasksViewModel.
  2. Ändern Sie den Konstruktor von TasksViewModel so, dass TasksRepository 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.

  1. Fügen Sie unten in der TasksViewModel-Datei außerhalb des Kurses ein TasksViewModelFactory-Element ein, das ein einfaches TasksRepository-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 ViewModels konstruiert werden. Nachdem Sie die Werksansicht erstellt haben, können Sie sie überall dort verwenden, wo Sie das Datenansichtsmodell erstellen.

  1. 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))
}
  1. 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.

  1. Öffnen Sie TasksViewModelTest.
  2. Füge in TasksViewModelTest eine Property FakeTestRepository 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
}
  1. Aktualisieren Sie die setupViewModel-Methode, um eine FakeTestRepository mit drei Aufgaben zu erstellen, und erstellen Sie dann die tasksViewModel 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)
        
    }
  1. Da du den AndroidX-Test ApplicationProvider.getApplicationContext nicht mehr verwendest, kannst du die @RunWith(AndroidJUnit4::class)-Annotation auch entfernen.
  2. 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.

  1. Öffnen Sie TaskDetailViewModel.
  2. 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 }
  1. Fügen Sie unten in der TaskDetailViewModel-Datei außerhalb des Kurses ein TaskDetailViewModelFactory-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)
}
  1. 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))
}
  1. 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

  1. 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-Testbibliothek
  • kotlinx-coroutines-test: Die Koroutinentestbibliothek
  • androidx.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.

  1. Öffnen Sie taskdetail.TaskDetailFragment.
  2. 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 den test-Quellsatz).

  1. 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).

  1. 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 ein FragmentScenario 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.

  1. Dies ist ein Instrumentierungstest. Prüfen Sie daher, ob der Emulator oder Ihr Gerät sichtbar ist.
  2. 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:

  1. Erstellen Sie eine Service Locator-Klasse, die ein Repository erstellen und speichern kann. Standardmäßig wird ein „&normal“-Repository erstellt.
  2. Refaktorieren Sie Ihren Code. Verwenden Sie dazu die Dienstsuche.
  3. 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.

  1. Erstellen Sie die Datei ServiceLocator.kt auf der obersten Ebene des Hauptquellsatzes.
  2. Definieren Sie einen object mit dem Namen ServiceLocator.
  3. Erstellen Sie die Instanzvariablen database und repository und setzen Sie beide auf null.
  4. Annotieren Sie das Repository @Volatile, da es von mehreren Threads verwendet werden kann (@Volatileausfü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:

  1. provideTasksRepository: Entweder stellt er ein bereits vorhandenes Repository bereit oder erstellt ein neues. Diese Methode sollte auf this synchronized lauten. In Situationen mit mehreren Threads wird nie versehentlich zwei Repository-Instanzen erstellt.
  2. createTasksRepository: Code zum Erstellen eines neuen Repositorys. Rufe createTaskLocalDataSource auf und erstellt ein neues TasksRemoteDataSource.
  3. createTaskLocalDataSource: Code zum Erstellen einer neuen lokalen Datenquelle Wird createDataBase angerufen.
  4. 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.

  1. Öffnen Sie auf der obersten Ebene der Pakethierarchie TodoApplication und erstellen Sie eine val für Ihr Repository. Weisen Sie diesem dann ein Repository zu, das Sie mit ServiceLocator.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.

  1. Ö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.

  1. Öffnen Sie TaskDetailFragement und suchen Sie oben im Kurs den Anruf bei getRepository.
  2. 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)
}
  1. 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)
    }
  1. Aktualisieren Sie den Code, der das Repository erwirbt, für StatisticsViewModel und AddEditTaskViewModel, damit es aus dem TodoApplication verwendet wird.

TasksFragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. 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.

  1. 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.
  2. Erstellen Sie in diesem Quellpaket eine neue Klasse mit dem Namen FakeAndroidTestRepository.kt.
  3. 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.

  1. Öffnen Sie ServiceLocator.kt.
  2. 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.

  1. Fügen Sie eine Instanzvariable namens lock mit dem Wert Any hinzu.

ServiceLocator.kt

private val lock = Any()
  1. 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.

  1. Öffnen Sie TaskDetailFragmentTest.
  2. Deklarieren Sie eine lateinit TasksRepository-Variable.
  3. 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()
    }
  1. Sie müssen den Funktionstext von activeTaskDetails_DisplayedInUi() in runBlockingTest umschließen.
  2. 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)

    }
  1. 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)

    }

}
  1. 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):

  1. Gehen Sie auf Ihrem Testgerät zu Einstellungen > Entwickleroptionen.
  2. 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:

1. Statische Espresso-Methode

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.

  1. Öffnen Sie TaskDetailFragmentTest.kt.
  2. 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
  1. Alles nach dem Kommentar von // THEN verwendet Espresso. Prüfe die Teststruktur und die Verwendung von withId und prüfe, ob die Detailseite korrekt ist.
  2. Führen Sie den Test aus und prüfen Sie, ob er bestanden wurde.

Schritt 4: Optional: eigenen Espressotest schreiben

Testen Sie nun selbst.

  1. 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
}
  1. Sehen Sie sich den vorherigen Test an und schließen Sie ihn ab.
  2. 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

  1. 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 wie DatePicker und RecyclerView enthalten. Außerdem finden Sie hier Bedienungshilfen-Prüfungen und die Klasse „CountingIdlingResource“, die später behandelt wird.

Schritt 2: Tasks-FragmentTest erstellen

  1. Öffnen Sie TasksFragment.
  2. 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.
  3. 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.

  1. 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)
        
    }
  1. 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.

  1. Macht das neue Modell zu dem Fragment NavController.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. 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.

  1. 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")
    )
}
  1. Testen Sie es.

Sie haben folgende Möglichkeiten, um die Navigation zu testen:

  1. Mit Mockito wird ein NavController-Mock erstellt.
  2. Hängen Sie den simulierten NavController an das Fragment an.
  3. 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.

  1. Schreiben Sie den Test clickAddTaskButton_navigateToAddEditFragment. Dabei wird geprüft, ob die AddEditTaskFragment 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.

Zip herunterladen

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:

Videos:

Sonstiges:

Links zu weiteren Codelabs in diesem Kurs finden Sie auf der Landingpage des erweiterten Android-Tools in Kotlin.