Einführung in Test Doubles und Dependency Injection

Dieses Codelab ist Teil des Kurses „Advanced Android in Kotlin“. Sie können den größten Nutzen aus diesem Kurs ziehen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Das ist jedoch nicht zwingend erforderlich. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu „Android für Fortgeschrittene mit Kotlin“ aufgeführt.

Einführung

In diesem zweiten Codelab zum Testen geht es um Test-Doubles: wann sie in Android verwendet werden sollten und wie sie mit Dependency Injection, dem Service Locator-Muster und Bibliotheken implementiert werden. Dabei lernen Sie, wie Sie Folgendes schreiben:

  • Repository-Einheitentests
  • Integrationstests für Fragmente und ViewModels
  • Fragmentnavigationstests

Was Sie bereits wissen sollten

Sie sollten mit Folgendem vertraut sein:

Lerninhalte

  • Teststrategie planen
  • Test-Doubles erstellen und verwenden, insbesondere Fakes und Mocks
  • Manuelle Abhängigkeitsinjektion in Android für Unit- und Integrationstests verwenden
  • Service Locator-Muster anwenden
  • Repositorys, Fragmente, Ansichtsmodelle und die Navigationskomponente testen

Sie verwenden die folgenden Bibliotheken und Codekonzepte:

Aufgaben

  • Unittests für ein Repository mit einem Test-Double und Abhängigkeitsinjektion schreiben
  • Unittests für ein View-Modell mit einem Test-Double und Dependency Injection schreiben
  • Integrationstests für Fragmente und ihre ViewModels mit dem Espresso-UI-Test-Framework schreiben
  • Navigationstests mit Mockito und Espresso schreiben

In dieser Reihe von Codelabs arbeiten Sie mit der App „TO-DO Notes“. Mit der App können Sie Aufgaben aufschreiben, die Sie erledigen müssen, und sie werden in einer Liste angezeigt. Sie können sie dann als erledigt oder nicht erledigt markieren, filtern oder löschen.

Diese App ist in Kotlin geschrieben, hat einige Bildschirme, verwendet Jetpack-Komponenten und folgt der Architektur aus der Anleitung zur App-Architektur. Wenn Sie lernen, wie Sie diese App testen, können Sie auch Apps testen, die dieselben Bibliotheken und dieselbe Architektur verwenden.

Code herunterladen

Laden Sie zuerst den Code herunter:

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

Machen Sie sich anhand der Anleitung unten mit dem Code vertraut.

Schritt 1: Beispiel-App ausführen

Öffnen Sie die TO-DO-App in Android Studio und führen Sie sie aus. Es sollte kompiliert werden. So können Sie die App ausprobieren:

  • Erstellen Sie eine neue Aufgabe mit der schwebenden Aktionsschaltfläche „Pluszeichen“. Geben Sie zuerst einen Titel und dann zusätzliche Informationen zur Aufgabe ein. Speichern Sie sie mit der grünen schwebenden Aktionsschaltfläche.
  • Klicken Sie in der Aufgabenliste auf den Titel der gerade erledigten Aufgabe und sehen Sie sich auf dem Detailbildschirm für diese Aufgabe den Rest der Beschreibung an.
  • Setzen Sie in der Liste oder auf dem Detailbildschirm ein Häkchen in das Kästchen der Aufgabe, um den Status auf Abgeschlossen zu setzen.
  • Kehren Sie zum Aufgabenbildschirm zurück, öffnen Sie das Filtermenü und filtern Sie die Aufgaben nach dem Status Aktiv und Abgeschlossen.
  • Öffne die Navigationsleiste und klicke auf Statistiken.
  • Kehren Sie zum Übersichtsbildschirm zurück und wählen Sie im Navigationsmenü Abgeschlossene löschen aus, um alle Aufgaben mit dem Status Abgeschlossen zu löschen.

Schritt 2: Beispiel-App-Code ansehen

Die TO-DO-App basiert auf dem beliebten Test- und Architekturbeispiel Architecture Blueprints (mit der reaktiven Architektur-Version des Beispiels). Die App folgt der Architektur aus dem Leitfaden zur App-Architektur. Es verwendet ViewModels mit Fragments, ein Repository und Room. Wenn Sie mit einem der folgenden Beispiele vertraut sind, hat diese App eine ähnliche Architektur:

Es ist wichtiger, dass Sie die allgemeine Architektur der App verstehen, als dass Sie die Logik auf einer bestimmten Ebene genau kennen.

Hier ist eine Zusammenfassung der Pakete:

Paket : com.example.android.architecture.blueprints.todoapp

.addedittask

Bildschirm zum Hinzufügen oder Bearbeiten einer Aufgabe:UI-Ebene-Code zum Hinzufügen oder Bearbeiten einer Aufgabe.

.data

Die Datenschicht:Hier geht es um die Datenschicht der Aufgaben. Sie enthält den Datenbank-, Netzwerk- und Repository-Code.

.statistics

Statistikbildschirm:UI-Layer-Code für den Statistikbildschirm.

.taskdetail

Aufgabendetailansicht:UI-Ebene-Code für eine einzelne Aufgabe.

.tasks

Aufgabenbildschirm:UI-Layer-Code für die Liste aller Aufgaben.

.util

Hilfsklassen:Gemeinsam genutzte Klassen, die in verschiedenen Teilen der App verwendet werden, z.B. für das Layout zum Aktualisieren per Wischen, das auf mehreren Bildschirmen verwendet wird.

Datenschicht (.data)

Diese App enthält eine simulierte Netzwerkschicht im Paket remote und eine Datenschicht im Paket local. Der Einfachheit halber wird die Netzwerkschicht in diesem Projekt nur mit einem HashMap mit einer Verzögerung simuliert, anstatt echte Netzwerkanfragen zu stellen.

Die DefaultTasksRepository-Koordinaten oder ‑Vermittlungen zwischen der Netzwerk- und der Datenbankebene und geben Daten an die UI-Ebene zurück.

UI-Ebene ( .addedittask, .statistics, .taskdetail, .tasks)

Jedes der UI-Layer-Pakete enthält ein Fragment und ein View-Modell sowie alle anderen Klassen, die für die Benutzeroberfläche erforderlich sind, z. B. einen Adapter für die Aufgabenliste. TaskActivity ist die Aktivität, die alle Fragmente enthält.

Navigation

Die Navigation für die App wird über die Navigationskomponente gesteuert. Sie ist in der Datei nav_graph.xml definiert. Die Navigation wird in den Ansichtsmodellen mit der Klasse Event ausgelöst. Die Ansichtsmodelle legen auch fest, welche Argumente übergeben werden sollen. Die Fragmente beobachten die Events und führen die tatsächliche Navigation zwischen den Bildschirmen durch.

In diesem Codelab erfahren Sie, wie Sie Repositories, Modelle und Fragmente mithilfe von Test Doubles und Dependency Injection testen. Bevor Sie sich ansehen, was das ist, sollten Sie sich mit den Gründen vertraut machen, die bestimmen, was und wie Sie diese Tests schreiben.

In diesem Abschnitt werden einige allgemeine Best Practices für Tests beschrieben, die für Android gelten.

Die Testpyramide

Bei der Entwicklung einer Teststrategie sind drei zusammenhängende Aspekte zu berücksichtigen:

  • Umfang: Wie viel Code wird durch den Test abgedeckt? Tests können für eine einzelne Methode, für die gesamte Anwendung oder für einen Teil davon ausgeführt werden.
  • Geschwindigkeit: Wie schnell wird der Test ausgeführt? Die Testgeschwindigkeit kann zwischen Millisekunden und mehreren Minuten variieren.
  • Realitätsnähe: Wie realitätsnah ist der Test? Wenn beispielsweise ein Teil des zu testenden Codes eine Netzwerkanfrage stellen muss, wird diese Anfrage dann tatsächlich vom Testcode gestellt oder wird das Ergebnis gefälscht? Wenn der Test tatsächlich mit dem Netzwerk kommuniziert, ist er zuverlässiger. Der Nachteil ist, dass der Test länger dauern kann, zu Fehlern führen kann, wenn das Netzwerk ausfällt, oder teuer sein kann.

Zwischen diesen Aspekten bestehen inhärente Kompromisse. Beispiel: Geschwindigkeit und Genauigkeit sind ein Kompromiss. Je schneller der Test, desto geringer ist in der Regel die Genauigkeit und umgekehrt. Eine gängige Methode zur Unterteilung automatisierter Tests ist die Einteilung in die folgenden drei Kategorien:

  • Einheitentests: Diese Tests sind sehr fokussiert und werden für eine einzelne Klasse ausgeführt, in der Regel für eine einzelne Methode in dieser Klasse. Wenn ein Unit-Test fehlschlägt, wissen Sie genau, wo in Ihrem Code das Problem liegt. Sie haben eine geringe Genauigkeit, da Ihre App in der Praxis viel mehr als die Ausführung einer Methode oder Klasse umfasst. Sie sind schnell genug, um jedes Mal ausgeführt zu werden, wenn Sie Ihren Code ändern. In den meisten Fällen handelt es sich um lokal ausgeführte Tests (im test-Quellset). Beispiel : Einzelne Methoden in Viewmodels und Repositories testen.
  • Integrationstests: Bei diesen Tests wird die Interaktion mehrerer Klassen getestet, um sicherzustellen, dass sie sich bei gemeinsamer Verwendung wie erwartet verhalten. Eine Möglichkeit, Integrationstests zu strukturieren, besteht darin, dass sie eine einzelne Funktion testen, z. B. die Möglichkeit, eine Aufgabe zu speichern. Sie testen einen größeren Codebereich als Unit-Tests, sind aber immer noch auf Geschwindigkeit und nicht auf vollständige Genauigkeit optimiert. Sie können je nach Situation entweder lokal oder als Instrumentationstests ausgeführt werden. Beispiel : Alle Funktionen eines einzelnen Fragment- und Viewmodel-Paars testen.
  • End-to-End-Tests (E2E): Testen Sie eine Kombination von Funktionen, die zusammenarbeiten. Sie testen große Teile der App, simulieren die tatsächliche Nutzung genau und sind daher in der Regel langsam. Sie haben die höchste Genauigkeit und zeigen Ihnen, dass Ihre Anwendung als Ganzes funktioniert. In der Regel handelt es sich dabei um instrumentierte Tests (im Quellset androidTest).
    Beispiel : Die gesamte App wird gestartet und einige Funktionen werden gemeinsam getestet.

Das empfohlene Verhältnis dieser Tests wird oft durch eine Pyramide dargestellt, wobei der Großteil der Tests Einheitentests sind.

Architektur und Tests

Ob Sie Ihre App auf allen Ebenen der Testpyramide testen können, hängt von der Architektur Ihrer App ab. Beispielsweise könnte eine extrem schlecht konzipierte Anwendung ihre gesamte Logik in einer Methode unterbringen. Sie können dafür möglicherweise einen End-to-End-Test schreiben, da diese Tests in der Regel große Teile der App abdecken. Aber wie sieht es mit Unit- oder Integrationstests aus? Da sich der gesamte Code an einem Ort befindet, ist es schwierig, nur den Code zu testen, der sich auf eine einzelne Einheit oder Funktion bezieht.

Ein besserer Ansatz wäre, die Anwendungslogik in mehrere Methoden und Klassen aufzuteilen, sodass jeder Teil isoliert getestet werden kann. Die Architektur ist eine Möglichkeit, Ihren Code aufzuteilen und zu organisieren, was Einheitentests und Integrationstests erleichtert. Die TO-DO-App, die Sie testen, folgt einer bestimmten Architektur:



In dieser Lektion erfahren Sie, wie Sie Teile der oben genannten Architektur isoliert testen:

  1. Zuerst führen Sie einen Unittest für das Repository aus.
  2. Anschließend verwenden Sie ein Test-Double im ViewModel, das für Unittests und Integrationstests des ViewModels erforderlich ist.
  3. Als Nächstes erfahren Sie, wie Sie Integrationstests für Fragmente und ihre ViewModels schreiben.
  4. Schließlich lernen Sie, Integrationstests zu schreiben, die die Navigation Component enthalten.

End-to-End-Tests werden in der nächsten Lektion behandelt.

Wenn Sie einen Einheitentest für einen Teil einer Klasse (eine Methode oder eine kleine Sammlung von Methoden) schreiben, sollten Sie nur den Code in dieser Klasse testen.

Es kann schwierig sein, nur Code in einer bestimmten Klasse oder in bestimmten Klassen zu testen. Sehen wir uns ein Beispiel an. Öffnen Sie die Klasse data.source.DefaultTaskRepository im Quellset main. Dies ist das Repository für die App und die Klasse, für die Sie als Nächstes Einheitentests schreiben.

Ihr Ziel ist es, nur den Code in dieser Klasse zu testen. DefaultTaskRepository ist jedoch von anderen Klassen wie LocalTaskDataSource und RemoteTaskDataSource abhängig. Anders ausgedrückt: LocalTaskDataSource und RemoteTaskDataSource sind Abhängigkeiten von DefaultTaskRepository.

Jede Methode in DefaultTaskRepository ruft also Methoden für Datenquellenklassen auf, die wiederum Methoden in anderen Klassen aufrufen, um Informationen in einer Datenbank zu speichern oder mit dem Netzwerk zu kommunizieren.



Sehen Sie sich beispielsweise diese Methode in DefaultTasksRepo an.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks ist einer der „einfachsten“ Aufrufe, die Sie an Ihr Repository senden können. Diese Methode umfasst das Lesen aus einer SQLite-Datenbank und das Ausführen von Netzwerkaufrufen (der Aufruf von updateTasksFromRemoteDataSource). Dies erfordert viel mehr Code als nur den Repository-Code.

Hier sind einige konkretere Gründe, warum das Testen des Repository schwierig ist:

  • Sie müssen sich Gedanken über das Erstellen und Verwalten einer Datenbank machen, um selbst die einfachsten Tests für dieses Repository durchzuführen. Daraus ergeben sich Fragen wie „Sollte dies ein lokaler oder instrumentierter Test sein?“ und „Sollte ich AndroidX Test verwenden, um eine simulierte Android-Umgebung zu erhalten?“.
  • Einige Teile des Codes, z. B. Netzwerkcode, können lange dauern oder gelegentlich sogar fehlschlagen, was zu lang andauernden, unzuverlässigen Tests führt.
  • Ihre Tests verlieren möglicherweise die Fähigkeit, zu diagnostizieren, welcher Code für einen Testfehler verantwortlich ist. Ihre Tests könnten mit dem Testen von Code beginnen, der nicht zum Repository gehört. So könnten beispielsweise Ihre vermeintlichen Unit-Tests für das Repository aufgrund eines Problems in einem der abhängigen Codes, z. B. dem Datenbankcode, fehlschlagen.

Test-Doubles

Die Lösung besteht darin, dass Sie beim Testen des Repositorys nicht den echten Netzwerk- oder Datenbankcode verwenden, sondern stattdessen ein Test-Double. Ein Test-Double ist eine Version einer Klasse, die speziell für Tests entwickelt wurde. Sie soll die reale Version einer Klasse in Tests ersetzen. Das ist ähnlich wie bei einem Stuntdouble, das ein Schauspieler ist, der sich auf Stunts spezialisiert hat und den eigentlichen Schauspieler bei gefährlichen Aktionen ersetzt.

Hier sind einige Arten von Test-Doubles:

Gefälscht

Ein Test-Double mit einer „funktionierenden“ Implementierung der Klasse, die jedoch so implementiert ist, dass sie sich gut für Tests, aber nicht für die Produktion eignet.

Mock

Ein Test-Double, das aufzeichnet, welche seiner Methoden aufgerufen wurden. Anschließend wird ein Test bestanden oder nicht bestanden, je nachdem, ob die Methoden richtig aufgerufen wurden.

Stub

Ein Test-Double, das keine Logik enthält und nur das zurückgibt, was Sie ihm vorgeben. Ein StubTaskRepository könnte so programmiert werden, dass bestimmte Kombinationen von Aufgaben aus getTasks zurückgegeben werden.

Dummy

Ein Test-Double, das übergeben, aber nicht verwendet wird, z. B. wenn Sie es nur als Parameter angeben müssen. Wenn Sie ein NoOpTaskRepository hätten, würde nur das TaskRepository mit keinem Code in einer der Methoden implementiert.

Spy

Ein Test-Double, das auch einige zusätzliche Informationen erfasst, z. B. wie oft die Methode addTask aufgerufen wurde, wenn Sie SpyTaskRepository erstellt haben.

Weitere Informationen zu Test-Doubles finden Sie unter Testing on the Toilet: Know Your Test Doubles.

Die am häufigsten in Android verwendeten Test-Doubles sind Fakes und Mocks.

In dieser Aufgabe erstellen Sie ein FakeDataSource-Test-Double, um DefaultTasksRepository unabhängig von den tatsächlichen Datenquellen zu testen.

Schritt 1: Klasse „FakeDataSource“ erstellen

In diesem Schritt erstellen Sie eine Klasse mit dem Namen FakeDataSouce, die ein Test-Double von LocalDataSource und RemoteDataSource ist.

  1. Klicken Sie im Quellset test mit der rechten Maustaste und wählen Sie Neu -> Paket aus.

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

Schritt 2: TasksDataSource-Schnittstelle implementieren

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

  1. Beachten Sie, dass beide die TasksDataSource-Schnittstelle implementieren.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Lassen Sie FakeDataSource TasksDataSource implementieren:
class FakeDataSource : TasksDataSource {

}

Android Studio meldet, dass Sie die erforderlichen Methoden für TasksDataSource nicht implementiert haben.

  1. Wählen Sie im Menü für schnelle Korrekturen Mitglieder implementieren aus.


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

Schritt 3: Methode „getTasks“ in „FakeDataSource“ implementieren

FakeDataSource ist ein bestimmter Typ von Test-Double, der als Fake bezeichnet wird. Ein Fake ist ein Test-Double, das eine „funktionierende“ Implementierung der Klasse hat. Diese Implementierung ist jedoch so gestaltet, dass sie sich gut für Tests, aber nicht für die Produktion eignet. Eine „funktionierende“ Implementierung bedeutet, dass die Klasse bei bestimmten Eingaben realistische Ausgaben erzeugt.

Ihre gefälschte Datenquelle stellt beispielsweise keine Verbindung zum Netzwerk her und speichert nichts in einer Datenbank. Stattdessen wird nur eine In-Memory-Liste verwendet. Das funktioniert wie erwartet, da Methoden zum Abrufen oder Speichern von Aufgaben die erwarteten Ergebnisse zurückgeben. Sie können diese Implementierung jedoch niemals in der Produktion verwenden, da sie nicht auf dem Server oder in einer Datenbank gespeichert wird.

Ein FakeDataSource

  • Damit können Sie den Code in DefaultTasksRepository testen, ohne auf eine echte Datenbank oder ein echtes Netzwerk angewiesen zu sein.
  • bietet eine „realistische“ Implementierung für Tests.
  1. Ändern Sie den FakeDataSource-Konstruktor, um ein var namens tasks zu erstellen, das ein MutableList<Task>? mit dem Standardwert einer leeren veränderlichen Liste ist.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Dies ist die Liste der Aufgaben, die eine Datenbank- oder Serverantwort „vortäuschen“. Das Ziel ist es, die getTasks-Methode des Repositorys zu testen. Dadurch werden die Methoden getTasks, deleteAllTasks und saveTask der Datenquelle aufgerufen.

Schreibe eine gefälschte Version dieser Methoden:

  1. Schreiben Sie getTasks: Wenn tasks nicht null ist, geben Sie ein Success-Ergebnis zurück. Wenn tasks null ist, wird ein Error-Ergebnis zurückgegeben.
  2. Schreiben Sie deleteAllTasks, um die Liste der veränderlichen Aufgaben zu löschen.
  3. Schreiben Sie saveTask, um die Aufgabe der Liste hinzuzufügen.

Diese Methoden, die für FakeDataSource implementiert wurden, sehen so aus:

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Hier sind die Importanweisungen, falls Sie sie benötigen:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Das funktioniert ähnlich wie bei den tatsächlichen lokalen und Remote-Datenquellen.

In diesem Schritt verwenden Sie eine Technik namens manuelle Abhängigkeitsinjektion, damit Sie das gerade erstellte Test-Double verwenden können.

Das Hauptproblem ist, dass Sie eine FakeDataSource haben, aber nicht klar ist, wie Sie sie in den Tests verwenden. Er muss TasksRemoteDataSource und TasksLocalDataSource ersetzen, aber nur in den Tests. Sowohl TasksRemoteDataSource als auch TasksLocalDataSource sind Abhängigkeiten von DefaultTasksRepository. Das bedeutet, dass DefaultTasksRepositories diese Klassen für die Ausführung benötigt oder von ihnen „abhängt“.

Derzeit werden die Abhängigkeiten in der Methode init von DefaultTasksRepository erstellt.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Da Sie taskLocalDataSource und tasksRemoteDataSource in DefaultTasksRepository erstellen und zuweisen, sind sie im Grunde fest codiert. Es gibt keine Möglichkeit, das Test-Double zu verwenden.

Stattdessen sollten Sie diese Datenquellen der Klasse zur Verfügung stellen, anstatt sie hart zu codieren. Das Bereitstellen von Abhängigkeiten wird als Abhängigkeitsinjektion bezeichnet. Es gibt verschiedene Möglichkeiten, Abhängigkeiten bereitzustellen, und daher auch verschiedene Arten der Abhängigkeitsinjektion.

Mit der Constructor Dependency Injection (Konstruktor-Dependency Injection) können Sie das Test-Double einfügen, indem Sie es an den Konstruktor übergeben.

Kein Einschleusen

Einschleusung

Schritt 1: Constructor Dependency Injection in DefaultTasksRepository verwenden

  1. Ändern Sie den Konstruktor von DefaultTaskRepository so, dass er anstelle von Application sowohl Datenquellen als auch den Coroutine-Dispatcher akzeptiert. Letzteren müssen Sie auch für Ihre Tests austauschen. Das wird im dritten Lektionsabschnitt zu Coroutines genauer beschrieben.

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Da Sie die Abhängigkeiten übergeben haben, entfernen Sie die init-Methode. Sie müssen die Abhängigkeiten nicht mehr erstellen.
  2. Löschen Sie auch die alten Instanzvariablen. Sie definieren sie im Konstruktor:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Aktualisieren Sie schließlich die Methode getRepository, damit der neue Konstruktor verwendet wird:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Sie verwenden jetzt die Konstruktor-Abhängigkeitsinjektion.

Schritt 2: FakeDataSource in Tests verwenden

Da Ihr Code jetzt die Constructor Dependency Injection verwendet, können Sie Ihre gefälschte Datenquelle zum Testen von DefaultTasksRepository verwenden.

  1. Klicken Sie mit der rechten Maustaste auf den Klassennamen DefaultTasksRepository und wählen Sie Generate (Generieren) und dann Test (Test) aus.
  2. Folgen Sie der Anleitung, um DefaultTasksRepositoryTest im Quellsatz test zu erstellen.
  3. Fügen Sie oben in Ihrer neuen DefaultTasksRepositoryTest-Klasse die folgenden Mitgliedsvariablen hinzu, um die Daten in Ihren gefälschten Datenquellen darzustellen.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Erstellen Sie drei Variablen: zwei FakeDataSource-Mitgliedsvariablen (eine für jede Datenquelle für Ihr Repository) und eine Variable für die DefaultTasksRepository, die Sie testen möchten.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Erstellen Sie eine Methode zum Einrichten und Initialisieren eines testbaren DefaultTasksRepository. Für diesen DefaultTasksRepository wird Ihr Test-Double FakeDataSource verwendet.

  1. Erstellen Sie eine Methode namens createRepository und annotieren Sie sie mit @Before.
  2. Instanziieren Sie Ihre gefälschten Datenquellen mit den Listen remoteTasks und localTasks.
  3. Instanziieren Sie tasksRepository mit den beiden gefälschten Datenquellen, die Sie gerade erstellt haben, und Dispatchers.Unconfined.

Die endgültige Methode sollte wie im Code unten aussehen.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Schritt 3: Test für DefaultTasksRepository.getTasks() schreiben

Zeit, einen DefaultTasksRepository-Test zu schreiben.

  1. Schreiben Sie einen Test für die Methode getTasks des Repositorys. Prüfen Sie, ob beim Aufrufen von getTasks mit true (d. h. beim Neuladen aus der Remote-Datenquelle) Daten aus der Remote-Datenquelle (und nicht aus der lokalen Datenquelle) zurückgegeben werden.

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

Beim Aufrufen von getTasks: wird ein Fehler ausgegeben.

Schritt 4: runBlockingTest hinzufügen

Der Coroutine-Fehler ist zu erwarten, da getTasks eine suspend-Funktion ist und Sie eine Coroutine starten müssen, um sie aufzurufen. Dazu benötigen Sie einen Coroutine-Scope. Um diesen Fehler zu beheben, müssen Sie einige Gradle-Abhängigkeiten für das Starten von Coroutinen in Ihren Tests hinzufügen.

  1. Fügen Sie dem Test-Quellset mit testImplementation die erforderlichen Abhängigkeiten für das Testen von Coroutinen hinzu.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Vergiss nicht, deine Daten zu synchronisieren!

kotlinx-coroutines-test ist die Coroutines-Testbibliothek, die speziell für das Testen von Coroutines entwickelt wurde. Verwenden Sie die Funktion runBlockingTest, um Ihre Tests auszuführen. Dies ist eine Funktion, die von der Coroutines-Testbibliothek bereitgestellt wird. Es wird ein Codeblock als Eingabe verwendet und dieser Codeblock wird dann in einem speziellen Coroutinenkontext ausgeführt, der synchron und sofort ausgeführt wird. Das bedeutet, dass Aktionen in einer deterministischen Reihenfolge ausgeführt werden. Dadurch werden Ihre Coroutinen im Grunde wie Nicht-Coroutinen ausgeführt. Diese Funktion ist also für das Testen von Code vorgesehen.

Verwenden Sie runBlockingTest in Ihren Testklassen, wenn Sie eine suspend-Funktion aufrufen. Im nächsten Codelab dieser Reihe erfahren Sie mehr darüber, wie runBlockingTest funktioniert und wie Sie Coroutinen testen können.

  1. Fügen Sie @ExperimentalCoroutinesApi über der Klasse hinzu. Damit wird ausgedrückt, dass Sie wissen, dass Sie in der Klasse eine experimentelle Coroutine-API (runBlockingTest) verwenden. Andernfalls erhalten Sie eine Warnung.
  2. Fügen Sie in Ihrem DefaultTasksRepositoryTest runBlockingTest hinzu, damit der gesamte Test als Codeblock betrachtet wird.

Dieser letzte Test sieht so aus:

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Führen Sie den neuen getTasks_requestsAllTasksFromRemoteDataSource-Test aus und prüfen Sie, ob er funktioniert und der Fehler behoben ist.

Sie haben gerade gesehen, wie Sie ein Repository unit-testen. In den nächsten Schritten verwenden Sie noch einmal die Abhängigkeitsinjektion und erstellen ein weiteres Test-Double. Diesmal wird gezeigt, wie Sie Unit- und Integrationstests für Ihre ViewModels schreiben.

Bei Einheitentests sollte nur die Klasse oder Methode getestet werden, die Sie interessiert. Dies wird als Isolation bezeichnet. Dabei wird die „Einheit“ klar isoliert und nur der Code getestet, der Teil dieser Einheit ist.

TasksViewModelTest sollte also nur TasksViewModel-Code testen und nicht in Datenbank-, Netzwerk- oder Repository-Klassen. Daher erstellen Sie für Ihre Ansichtsmodelle, wie gerade für Ihr Repository, ein gefälschtes Repository und wenden die Abhängigkeitsinjektion an, um es in Ihren Tests zu verwenden.

In dieser Aufgabe wenden Sie die Abhängigkeitsinjektion auf Ansichtsmodelle an.

Schritt 1: TasksRepository-Schnittstelle erstellen

Der erste Schritt zur Verwendung der Constructor Dependency Injection besteht darin, eine gemeinsame Schnittstelle zu erstellen, die von der Fake- und der echten Klasse verwendet wird.

Wie sieht das in der Praxis aus? Sehen Sie sich TasksRemoteDataSource, TasksLocalDataSource und FakeDataSource an. Sie alle haben dieselbe Schnittstelle: TasksDataSource. So können Sie im Konstruktor von DefaultTasksRepository angeben, dass Sie ein TasksDataSource verwenden.

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

So können wir FakeDataSource einfügen.

Erstellen Sie als Nächstes eine Schnittstelle für DefaultTasksRepository, wie Sie es für die Datenquellen getan haben. Sie muss alle öffentlichen Methoden (öffentliche API-Oberfläche) von DefaultTasksRepository enthalten.

  1. Öffnen Sie DefaultTasksRepository und klicken Sie mit der rechten Maustaste auf den Kursnamen. Wählen Sie dann Refactor -> Extract -> Interface (Umgestalten -> 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. Setzen Sie im Bereich Mitglieder für die Formularschnittstelle ein Häkchen bei allen Mitgliedern außer den beiden Companion-Mitgliedern und den privaten Methoden.


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

Und DefaultTasksRepository implementiert jetzt TasksRepository.

  1. Führen Sie Ihre App aus (nicht die Tests), um zu prüfen, ob alles noch funktioniert.

Schritt 2: FakeTestRepository erstellen

Nachdem Sie die Schnittstelle haben, können Sie das DefaultTaskRepository-Test-Double erstellen.

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

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Sie werden aufgefordert, die Schnittstellenmethoden zu implementieren.

  1. Bewegen Sie den Mauszeiger auf den Fehler, bis das Vorschlagsmenü angezeigt wird, klicken Sie dann auf Implement members (Mitglieder implementieren) und wählen Sie die Option aus.
  1. Wählen Sie alle Methoden aus und drücken Sie OK.

Schritt 3: FakeTestRepository-Methoden implementieren

Sie haben jetzt eine FakeTestRepository-Klasse mit Methoden, die nicht implementiert sind. Ähnlich wie bei der Implementierung von FakeDataSource wird FakeTestRepository von einer Datenstruktur unterstützt, anstatt dass eine komplizierte Vermittlung zwischen lokalen und Remote-Datenquellen erforderlich ist.

Für Ihr FakeTestRepository sind keine FakeDataSources oder Ähnliches erforderlich. Es muss lediglich realistische gefälschte Ausgaben für die Eingaben zurückgeben. Sie verwenden ein LinkedHashMap zum Speichern der Liste der Aufgaben und ein MutableLiveData für die beobachtbaren Aufgaben.

  1. Fügen Sie in FakeTestRepository sowohl eine LinkedHashMap-Variable für die aktuelle Liste der Aufgaben als auch eine MutableLiveData für die beobachtbaren Aufgaben hinzu.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Implementieren Sie die folgenden Methoden:

  1. getTasks: Diese Methode sollte tasksServiceData in eine Liste umwandeln (mit tasksServiceData.values.toList()) und das Ergebnis als Success zurückgeben.
  2. refreshTasks: Aktualisiert den Wert von observableTasks auf den Wert, der von getTasks() zurückgegeben wird.
  3. observeTasks: Erstellt eine Coroutine mit runBlocking und führt refreshTasks aus. Gibt dann observableTasks zurück.

Unten finden Sie den Code für diese Methoden.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

Schritt 4: Methode zum Testen von „addTasks“ hinzufügen

Beim Testen ist es besser, wenn sich bereits einige Tasks in Ihrem Repository befinden. Sie könnten saveTask mehrmals aufrufen. Um dies zu vereinfachen, fügen Sie eine Hilfsmethode speziell für Tests hinzu, mit der Sie Aufgaben hinzufügen können.

  1. Fügen Sie die Methode addTasks hinzu, die eine vararg mit Aufgaben entgegennimmt, jede Aufgabe der HashMap hinzufügt und dann die Aufgaben aktualisiert.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

Sie haben jetzt ein gefälschtes Repository zum Testen mit einigen der wichtigsten implementierten Methoden. Verwenden Sie sie dann in Ihren Tests.

In dieser Aufgabe verwenden Sie eine gefälschte Klasse in einem ViewModel. Verwenden Sie die Constructor Dependency Injection, um die beiden Datenquellen über die Constructor Dependency Injection zu übernehmen. Fügen Sie dazu dem Konstruktor von TasksViewModel eine TasksRepository-Variable hinzu.

Bei Viewmodels ist dieser Prozess etwas anders, da Sie sie nicht direkt erstellen. Beispiel:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Wie im obigen Code verwenden Sie den viewModel's Property-Delegate, der das ViewModel erstellt. Wenn Sie ändern möchten, wie das Ansichtsmodell erstellt wird, müssen Sie ein ViewModelProvider.Factory hinzufügen und verwenden. Wenn Sie ViewModelProvider.Factory noch nicht kennen, finden Sie hier weitere Informationen.

Schritt 1: ViewModelFactory in TasksViewModel erstellen und verwenden

Beginnen Sie mit der Aktualisierung der Klassen und Tests für den Bildschirm Tasks.

  1. TasksViewModel öffnen
  2. Ändern Sie den Konstruktor von TasksViewModel so, dass er TasksRepository akzeptiert, anstatt ihn in der Klasse zu erstellen.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Da Sie den Konstruktor geändert haben, müssen Sie jetzt eine Factory verwenden, um TasksViewModel zu erstellen. Die Factory-Klasse kann in derselben Datei wie TasksViewModel oder in einer eigenen Datei gespeichert werden.

  1. Fügen Sie unten in der Datei TasksViewModel außerhalb der Klasse ein TasksViewModelFactory ein, das ein einfaches TasksRepository akzeptiert.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


So ändern Sie die Konstruktion von ViewModel. Jetzt, da Sie die Factory haben, können Sie sie überall verwenden, wo Sie Ihr View-Modell erstellen.

  1. TasksFragment aktualisieren, um die Factory zu verwenden

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Führen Sie den Code Ihrer App aus und prüfen Sie, ob alles noch funktioniert.

Schritt 2: FakeTestRepository in TasksViewModelTest verwenden

Anstelle des echten Repositorys können Sie jetzt das gefälschte Repository in Ihren Viewmodel-Tests verwenden.

  1. Öffnen Sie TasksViewModelTest.
  2. Fügen Sie dem TasksViewModelTest das Attribut 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 Methode setupViewModel, um eine FakeTestRepository mit drei Aufgaben zu erstellen, und erstellen Sie dann das 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 Sie den AndroidX Test-Code ApplicationProvider.getApplicationContext nicht mehr verwenden, können Sie auch die Annotation @RunWith(AndroidJUnit4::class) entfernen.
  2. Führen Sie Ihre Tests aus und prüfen Sie, ob sie alle noch funktionieren.

Durch die Verwendung der Constructor-Dependency-Injection haben Sie die DefaultTasksRepository als Abhängigkeit entfernt und in den Tests durch Ihre FakeTestRepository ersetzt.

Schritt 3: Auch TaskDetail-Fragment und -ViewModel aktualisieren

Nehmen Sie genau dieselben Änderungen für TaskDetailFragment und TaskDetailViewModel vor. So wird der Code für die TaskDetail-Tests vorbereitet, die Sie als Nächstes schreiben.

  1. TaskDetailViewModel öffnen
  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 Datei TaskDetailViewModel außerhalb der Klasse eine TaskDetailViewModelFactory hinzu.

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. TasksFragment aktualisieren, um die Factory zu verwenden

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Führen Sie Ihren Code aus und prüfen Sie, ob alles funktioniert.

Sie können jetzt in TasksFragment und TasksDetailFragment ein FakeTestRepository anstelle des tatsächlichen Repositorys verwenden.

Als Nächstes schreiben Sie Integrationstests, um die Interaktionen zwischen Fragment und ViewModel zu testen. Sie erfahren, ob Ihr Viewmodel-Code die Benutzeroberfläche richtig aktualisiert. Dazu verwenden Sie

  • das ServiceLocator-Muster
  • Espresso- und Mockito-Bibliotheken

Integrationstests prüfen die Interaktion mehrerer Klassen, um sicherzustellen, dass sie sich bei gemeinsamer Verwendung wie erwartet verhalten. Diese Tests können entweder lokal (test-Quellgruppe) oder als Instrumentierungstests (androidTest-Quellgruppe) ausgeführt werden.

In Ihrem Fall nehmen Sie jedes Fragment und schreiben Integrationstests für das Fragment und das View-Modell, um die Hauptfunktionen des Fragments zu testen.

Schritt 1: Gradle-Abhängigkeiten hinzufügen

  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 zum Schreiben grundlegender Testanweisungen erforderlich ist.
  • androidx.test:core – Core AndroidX-Testbibliothek
  • kotlinx-coroutines-test – Die Coroutines-Testbibliothek
  • androidx.fragment:fragment-testing: AndroidX-Testbibliothek zum Erstellen von Fragmenten in Tests und zum Ändern ihres Status.

Da Sie diese Bibliotheken in Ihrem androidTest-Quellsatz verwenden, fügen Sie sie mit androidTestImplementation als Abhängigkeiten hinzu.

Schritt 2: Klasse „TaskDetailFragmentTest“ erstellen

Die TaskDetailFragment enthält Informationen zu einer einzelnen Aufgabe.

Sie beginnen mit dem Schreiben eines Fragmenttests für TaskDetailFragment, da es im Vergleich zu den anderen Fragmenten relativ einfache Funktionen hat.

  1. taskdetail.TaskDetailFragment öffnen
  2. Generieren Sie einen Test für TaskDetailFragment wie zuvor. Akzeptieren Sie die Standardauswahl und legen Sie sie im Quellsatz androidTest ab (NICHT im Quellsatz test).

  1. Fügen Sie der Klasse TaskDetailFragmentTest die folgenden Annotationen hinzu.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

Der Zweck dieser Anmerkungen ist:

  • @MediumTest: Kennzeichnet den Test als Integrationstest mit mittlerer Laufzeit (im Gegensatz zu @SmallTest-Unit-Tests und @LargeTest-End-to-End-Tests). So können Sie die Tests gruppieren und die Größe des Tests auswählen, den Sie ausführen möchten.
  • @RunWith(AndroidJUnit4::class): Wird in jeder Klasse verwendet, in der AndroidX Test verwendet wird.

Schritt 3: Fragment über einen Test starten

In dieser Aufgabe starten Sie TaskDetailFragment mit der AndroidX Testing-Bibliothek. FragmentScenario ist eine Klasse aus AndroidX Test, die ein Fragment umschließt und Ihnen die direkte Steuerung des Fragmentlebenszyklus für Tests ermöglicht. Um Tests für Fragmente zu schreiben, erstellen Sie ein FragmentScenario für das Fragment, das Sie testen (TaskDetailFragment).

  1. Kopieren Sie diesen Test in TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Dieser Code oben:

  • Erstellt eine Aufgabe.
  • Erstellt ein Bundle, das die Fragmentargumente für die Aufgabe darstellt, die an das Fragment übergeben werden.
  • Die Funktion launchFragmentInContainer erstellt mit diesem Bundle und einem Design ein FragmentScenario.

Das ist noch kein vollständiger Test, da nichts bestätigt wird. Führen Sie den Test erst einmal aus und beobachten Sie, was passiert.

  1. Da es sich um einen instrumentierten Test handelt, muss der Emulator oder Ihr Gerät sichtbar sein.
  2. Führen Sie den Test aus.

Es sollten einige Dinge passieren.

  • Da es sich um einen instrumentierten Test handelt, wird er entweder auf Ihrem physischen Gerät (falls verbunden) oder auf einem Emulator ausgeführt.
  • Dadurch sollte das Fragment gestartet werden.
  • Es wird kein anderes Fragment aufgerufen und es sind keine Menüs mit der Aktivität verknüpft – es ist nur das Fragment.

Sehen Sie sich das Fragment genau an. Es wird „Keine Daten“ angezeigt, da die Aufgabendaten nicht geladen werden konnten.

In Ihrem Test muss sowohl TaskDetailFragment geladen als auch bestätigt werden, dass die Daten korrekt geladen wurden. Warum sind keine Daten vorhanden? Das liegt daran, dass Sie eine Aufgabe erstellt, aber nicht im Repository gespeichert haben.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Sie haben dieses FakeTestRepository, aber Sie benötigen eine Möglichkeit, Ihr echtes Repository für Ihr Fragment durch das gefälschte zu ersetzen. Das ist der nächste Schritt.

In dieser Aufgabe stellen Sie Ihrem Fragment Ihr gefälschtes Repository über ein ServiceLocator zur Verfügung. So können Sie Ihre Fragment- und Viewmodel-Integrationstests schreiben.

Sie können hier keine Constructor Dependency Injection verwenden, wie Sie es zuvor getan haben, als Sie eine Abhängigkeit für das View-Modell oder das Repository bereitstellen mussten. Für die Constructor Dependency Injection muss die Klasse erstellt werden. Fragmente und Aktivitäten sind Beispiele für Klassen, die Sie nicht erstellen und auf deren Konstruktor Sie in der Regel keinen Zugriff haben.

Da Sie das Fragment nicht erstellen, können Sie die Constructor Dependency Injection nicht verwenden, um das Repository-Test-Double (FakeTestRepository) mit dem Fragment zu tauschen. Verwenden Sie stattdessen das Service Locator-Muster. Das Service Locator-Muster ist eine Alternative zur Abhängigkeitsinjektion. Dazu wird eine Singleton-Klasse namens „Service Locator“ erstellt, die sowohl für den regulären als auch für den Testcode Abhängigkeiten bereitstellt. Im regulären App-Code (der main-Quellsatz) sind alle diese Abhängigkeiten die regulären App-Abhängigkeiten. Für die Tests ändern Sie den Service Locator, um Test-Double-Versionen der Abhängigkeiten bereitzustellen.

Service Locator nicht verwenden


Service Locator verwenden

Gehen Sie für diese Codelab-App so vor:

  1. Erstellen Sie eine Service Locator-Klasse, die ein Repository erstellen und speichern kann. Standardmäßig wird ein „normales“ Repository erstellt.
  2. Lagern Sie Ihren Code so um, dass der Service Locator verwendet wird, wenn Sie ein Repository benötigen.
  3. Rufen Sie in Ihrer Testklasse eine Methode für den Service Locator auf, die das „normale“ Repository durch Ihr Test-Double ersetzt.

Schritt 1: ServiceLocator erstellen

Erstellen wir eine ServiceLocator-Klasse. Sie befindet sich mit dem restlichen App-Code im Hauptquellsatz, da sie vom Hauptanwendungscode verwendet wird.

Hinweis: ServiceLocator ist ein Singleton. Verwenden Sie daher das Kotlin-Schlüsselwort object für die Klasse.

  1. Erstellen Sie die Datei ServiceLocator.kt auf der obersten Ebene des Hauptquellsets.
  2. Definieren Sie eine object mit dem Namen ServiceLocator.
  3. Erstellen Sie die Instanzvariablen database und repository und legen Sie beide auf null fest.
  4. Kommentieren Sie das Repository mit @Volatile, da es von mehreren Threads verwendet werden könnte (@Volatile wird hier ausführlich erläutert).

Ihr Code sollte wie unten aussehen.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Derzeit muss Ihre ServiceLocator nur wissen, wie eine TasksRepository zurückgegeben wird. Es wird ein vorhandenes DefaultTasksRepository zurückgegeben oder bei Bedarf ein neues DefaultTasksRepository erstellt und zurückgegeben.

Definieren Sie die folgenden Funktionen:

  1. provideTasksRepository: Stellt entweder ein bereits vorhandenes Repository bereit oder erstellt ein neues. Diese Methode sollte synchronized für this sein, um zu vermeiden, dass in Situationen mit mehreren ausgeführten Threads versehentlich zwei Repository-Instanzen erstellt werden.
  2. createTasksRepository: Code zum Erstellen eines neuen Repositorys. Ruft createTaskLocalDataSource auf und erstellt eine neue TasksRemoteDataSource.
  3. createTaskLocalDataSource: Code zum Erstellen einer neuen lokalen Datenquelle. createDataBase wird angerufen.
  4. createDataBase: Code zum Erstellen einer neuen Datenbank.

Der vollständige Code ist unten zu sehen.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Schritt 2: ServiceLocator in der Anwendung verwenden

Sie nehmen eine Änderung an Ihrem Hauptanwendungscode (nicht an Ihren Tests) vor, damit Sie das Repository an einem Ort erstellen, nämlich in ServiceLocator.

Es ist wichtig, dass Sie immer nur eine Instanz der Repository-Klasse erstellen. Dazu verwenden Sie den Service Locator in Ihrer Application-Klasse.

  1. Öffnen Sie TodoApplication auf der obersten Ebene Ihrer Pakethierarchie und erstellen Sie ein val für Ihr Repository. Weisen Sie ihm ein Repository zu, das mit ServiceLocator.provideTaskRepository abgerufen wird.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Nachdem Sie ein Repository in der Anwendung erstellt haben, können Sie die alte getRepository-Methode in DefaultTasksRepository entfernen.

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

Verwenden Sie jetzt überall, wo Sie getRepository verwendet haben, stattdessen taskRepository der Anwendung. So wird sichergestellt, dass Sie nicht das Repository direkt erstellen, sondern das Repository erhalten, das vom ServiceLocator bereitgestellt wurde.

  1. Öffnen Sie TaskDetailFragement und suchen Sie oben in der Klasse nach dem Aufruf von getRepository.
  2. Ersetzen Sie diesen Aufruf durch einen Aufruf, der das Repository von TodoApplication abruft.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  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 für StatisticsViewModel und AddEditTaskViewModel den Code, mit dem das Repository abgerufen wird, sodass das Repository aus 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ühren Sie Ihre Anwendung (nicht den Test) aus.

Da Sie nur Refactoring durchgeführt haben, sollte die App problemlos laufen.

Schritt 3: FakeAndroidTestRepository erstellen

Sie haben bereits ein FakeTestRepository im Testquellenset. Testklassen können standardmäßig nicht zwischen den Quellsätzen test und androidTest geteilt werden. Sie müssen also eine doppelte FakeTestRepository-Klasse im Quellset androidTest erstellen und sie FakeAndroidTestRepository nennen.

  1. Klicken Sie mit der rechten Maustaste auf das Quellset androidTest und erstellen Sie ein Datenpaket. Klicken Sie noch einmal mit der rechten Maustaste und erstellen Sie ein Quellpaket .
  2. Erstellen Sie in diesem Quellpaket eine neue Klasse mit dem Namen FakeAndroidTestRepository.kt.
  3. Kopieren Sie den folgenden Code in diese Klasse.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Schritt 4: ServiceLocator für Tests vorbereiten

Okay, jetzt ist es an der Zeit, ServiceLocator zu verwenden, um beim Testen Test-Doubles einzusetzen. Dazu müssen Sie Ihrem ServiceLocator-Code Code hinzufügen.

  1. ServiceLocator.kt öffnen
  2. Markieren Sie den Setter für tasksRepository als @VisibleForTesting. Mit dieser Anmerkung wird ausgedrückt, dass der Setter aus Testzwecken öffentlich ist.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Unabhängig davon, ob Sie Ihren Test allein oder in einer Gruppe von Tests ausführen, sollten die Tests genau gleich ablaufen. Das bedeutet, dass Ihre Tests kein Verhalten aufweisen sollten, das voneinander abhängig ist. Sie sollten also keine Objekte zwischen Tests freigeben.

Da ServiceLocator ein Singleton ist, kann es versehentlich zwischen Tests geteilt werden. Um dies zu vermeiden, erstellen Sie eine Methode, die den ServiceLocator-Status zwischen den Tests richtig zurücksetzt.

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

ServiceLocator.kt

private val lock = Any()
  1. Fügen Sie eine testspezifische Methode namens resetRepository hinzu, die die Datenbank leert und sowohl das Repository als auch die Datenbank auf „null“ setzt.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Schritt 5: ServiceLocator verwenden

In diesem Schritt verwenden Sie ServiceLocator.

  1. TaskDetailFragmentTest öffnen
  2. Deklarieren Sie eine lateinit TasksRepository-Variable.
  3. Fügen Sie eine Setup- und eine Tear-Down-Methode hinzu, um vor jedem Test eine FakeAndroidTestRepository einzurichten und sie nach jedem Test zu bereinigen.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Schließen Sie den Funktionskörper von activeTaskDetails_DisplayedInUi() in runBlockingTest ein.
  2. Speichern Sie activeTask im Repository, bevor Sie das Fragment starten.
repository.saveTask(activeTask)

Der endgültige Test sieht so aus:

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Kommentieren Sie die gesamte Klasse mit @ExperimentalCoroutinesApi.

Wenn Sie fertig sind, sieht der Code so aus.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Führen Sie den activeTaskDetails_DisplayedInUi()-Test aus.

Wie zuvor sollte das Fragment angezeigt werden. Da Sie das Repository jedoch richtig eingerichtet haben, werden jetzt die Aufgabeninformationen angezeigt.


In diesem Schritt verwenden Sie die Espresso-UI-Testbibliothek, um Ihren ersten Integrationstest durchzuführen. Sie haben Ihren Code so strukturiert, dass Sie Tests mit Zusicherungen für Ihre Benutzeroberfläche hinzufügen können. Dazu verwenden Sie die Espresso-Testbibliothek.

Espresso bietet folgende Vorteile:

  • Interagieren Sie mit Ansichten, z. B. indem Sie auf Schaltflächen klicken, einen Balken verschieben oder auf einem Bildschirm nach unten scrollen.
  • Bestätigen, dass bestimmte Ansichten auf dem Bildschirm angezeigt werden oder sich in einem bestimmten Zustand befinden (z. B. dass sie bestimmten Text enthalten oder dass ein Kästchen angekreuzt ist).

Schritt 1: Gradle-Abhängigkeit

Die Hauptabhängigkeit von Espresso ist bereits vorhanden, da sie standardmäßig in Android-Projekte aufgenommen wird.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core: Diese Espresso-Kernabhängigkeit ist standardmäßig enthalten, wenn Sie ein neues Android-Projekt erstellen. Sie enthält den grundlegenden Testcode für die meisten Ansichten und Aktionen.

Schritt 2: Animationen deaktivieren

Espresso-Tests werden auf einem echten Gerät ausgeführt und sind daher Instrumentierungstests. Ein Problem, das auftreten kann, sind Animationen: Wenn eine Animation verzögert wird und Sie versuchen, zu testen, ob eine Ansicht auf dem Bildschirm angezeigt wird, sie aber noch animiert wird, kann Espresso einen Test versehentlich fehlschlagen lassen. Das kann dazu führen, dass Espresso-Tests instabil werden.

Für Espresso-UI-Tests empfiehlt es sich, Animationen zu deaktivieren. Dadurch werden Ihre Tests auch schneller ausgeführt:

  1. Rufen Sie auf Ihrem Testgerät die Einstellungen > Entwickleroptionen auf.
  2. Deaktivieren Sie die drei Einstellungen Fensteranimationsfaktor, Übergangsanimationsfaktor und Animationsdauerfaktor.

Schritt 3: Espresso-Test ansehen

Bevor Sie einen Espresso-Test schreiben, sollten Sie sich etwas Espresso-Code ansehen.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Mit dieser Anweisung wird die Checkbox-Ansicht mit der ID task_detail_complete_checkbox gesucht, darauf geklickt und dann bestätigt, dass sie aktiviert ist.

Die meisten Espresso-Anweisungen bestehen aus vier Teilen:

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 häufigsten, aber es gibt auch andere Optionen wie onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId ist ein Beispiel für eine ViewMatcher, die eine Ansicht anhand ihrer ID abruft. Es gibt weitere View-Matcher, die Sie in der Dokumentation nachschlagen können.

3. ViewAction

perform(click())

Die Methode perform, die ein ViewAction akzeptiert. Eine ViewAction ist eine Aktion, die für die Ansicht ausgeführt werden kann, z. B. das Klicken auf die Ansicht.

4. ViewAssertion

check(matches(isChecked()))

check, das ein ViewAssertion akzeptiert. ViewAssertions check or asserts something about the view. Die am häufigsten verwendete ViewAssertion ist die matches-Assertion. Verwenden Sie zum Abschließen der Assertion ein anderes ViewMatcher, in diesem Fall isChecked.

Beachten Sie, dass Sie in einer Espresso-Anweisung nicht immer sowohl perform als auch check aufrufen. Sie können Anweisungen haben, die nur eine Behauptung mit check aufstellen, oder nur eine ViewAction mit perform ausführen.

  1. TaskDetailFragmentTest.kt öffnen
  2. Aktualisieren Sie den activeTaskDetails_DisplayedInUi-Test.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Hier sind die Importanweisungen, falls erforderlich:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Alles nach dem // THEN-Kommentar verwendet Espresso. Sehen Sie sich die Teststruktur und die Verwendung von withId an und treffen Sie Aussagen dazu, wie die Detailseite aussehen sollte.
  2. Führen Sie den Test aus und bestätigen Sie, dass er bestanden wurde.

Schritt 4: Optional: Eigenen Espresso-Test schreiben

Schreiben Sie nun selbst einen Test.

  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 diesen Test ab.
  2. Führen Sie den Test aus und bestätigen Sie, dass er bestanden wurde.

Die fertige completedTaskDetails_DisplayedInUi sollte so aussehen.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

In diesem letzten Schritt erfahren Sie, wie Sie die Navigation-Komponente mit einem anderen Typ von Test-Double, einem Mock, und der Testbibliothek Mockito testen.

In diesem Codelab haben Sie ein Test-Double namens „Fake“ verwendet. Fakes sind eine von vielen Arten von Test-Doubles. Welches Test-Double sollten Sie zum Testen der Navigation Component verwenden?

Überlegen Sie, wie die Navigation erfolgt. Stellen Sie sich vor, Sie drücken eine der Aufgaben in TasksFragment, um den Bildschirm mit den Aufgabendetails aufzurufen.

Hier ist der Code in TasksFragment, der bei einem Tastendruck zum Bildschirm mit den Aufgabendetails wechselt.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


Die Navigation erfolgt aufgrund eines Aufrufs der Methode navigate. Wenn Sie eine Assert-Anweisung schreiben mussten, gibt es keine einfache Möglichkeit, zu testen, ob Sie zu TaskDetailFragment navigiert sind. Die Navigation ist eine komplizierte Aktion, die über die Initialisierung von TaskDetailFragment hinaus nicht zu einer klaren Ausgabe oder Zustandsänderung führt.

Sie können bestätigen, dass die Methode navigate mit dem richtigen Aktionsparameter aufgerufen wurde. Genau das macht ein Mock-Test-Double: Es prüft, ob bestimmte Methoden aufgerufen wurden.

Mockito ist ein Framework zum Erstellen von Test-Doubles. Obwohl das Wort „mock“ in der API und im Namen verwendet wird, dient es nicht nur zum Erstellen von Mockups. Außerdem können Stubs und Spies erstellt werden.

Sie verwenden Mockito, um ein Mock-Objekt für NavigationController zu erstellen, mit dem Sie bestätigen können, dass die Methode „navigate“ korrekt aufgerufen wurde.

Schritt 1: Gradle-Abhängigkeiten hinzufügen

  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: Dies ist die Mockito-Abhängigkeit.
  • dexmaker-mockito: Diese Bibliothek ist erforderlich, um Mockito in einem Android-Projekt zu verwenden. Mockito muss Klassen zur Laufzeit generieren. Unter Android erfolgt dies mit DEX-Bytecode. Mit dieser Bibliothek kann Mockito also Objekte zur Laufzeit unter Android generieren.
  • androidx.test.espresso:espresso-contrib: Diese Bibliothek besteht aus externen Beiträgen (daher der Name), die Testcode für komplexere Ansichten wie DatePicker und RecyclerView enthalten. Es enthält auch Bedienungshilfen-Prüfungen und die Klasse CountingIdlingResource, die später behandelt wird.

Schritt 2: TasksFragmentTest erstellen

  1. Öffnen Sie TasksFragment.
  2. Klicken Sie mit der rechten Maustaste auf den Klassennamen TasksFragment und wählen Sie Generate (Generieren) und dann Test (Test) aus. Erstellen Sie einen Test im Quellsatz androidTest.
  3. Kopieren Sie diesen Code in die Datei TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Dieser Code ähnelt dem TaskDetailFragmentTest-Code, den Sie geschrieben haben. Es richtet ein FakeAndroidTestRepository ein und baut es wieder ab. Fügen Sie einen Navigationstest hinzu, um zu prüfen, ob Sie beim Klicken auf eine Aufgabe in der Aufgabenliste zum richtigen TaskDetailFragment weitergeleitet werden.

  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. Verwenden Sie die Mockito-Funktion mock, um ein Mock-Objekt zu erstellen.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

Um in Mockito ein Mock-Objekt zu erstellen, übergeben Sie die Klasse, die Sie simulieren möchten.

Als Nächstes müssen Sie Ihr NavController mit dem Fragment verknüpfen. Mit onFragment können Sie Methoden für das Fragment selbst aufrufen.

  1. Machen Sie das neue Mock zum NavController des Fragments.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Fügen Sie den Code hinzu, um auf das Element in RecyclerView zu klicken, das den Text „TITLE1“ enthält.
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions ist Teil der espresso-contrib-Bibliothek und ermöglicht es Ihnen, Espresso-Aktionen für eine RecyclerView auszuführen.

  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 macht dies zu einem Mock. Sie können bestätigen, dass für das gemockte navController eine bestimmte Methode (navigate) mit einem Parameter (actionTasksFragmentToTaskDetailFragment mit der ID „id1“) aufgerufen wurde.

Der vollständige Test sieht so aus:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Test starten

Zusammenfassend lässt sich sagen, dass Sie die Navigation so testen können:

  1. Verwenden Sie Mockito, um ein NavController-Mock zu erstellen.
  2. Hängen Sie das simulierte NavController an das Fragment an.
  3. Prüfen Sie, ob „navigate“ mit der richtigen Aktion und den richtigen Parametern aufgerufen wurde.

Schritt 3: Optional: write clickAddTaskButton_navigateToAddEditFragment

Wenn Sie selbst einen Navigationstest schreiben möchten, versuchen Sie es mit dieser Aufgabe.

  1. Schreibe den Test clickAddTaskButton_navigateToAddEditFragment, der prüft, ob du durch Klicken auf das + FAB zur AddEditTaskFragment gelangst.

Die Antwort finden Sie unten.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

Klicken Sie hier, um einen Vergleich zwischen dem Code, mit dem Sie begonnen haben, und dem endgültigen Code zu sehen.

Wenn Sie den Code für das fertige Codelab herunterladen möchten, können Sie den folgenden Git-Befehl verwenden:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


Alternativ können Sie das Repository als ZIP-Datei herunterladen, entzippen und in Android Studio öffnen.

Zip herunterladen

In diesem Codelab haben Sie gelernt, wie Sie die manuelle Abhängigkeitsinjektion und einen Service Locator einrichten und wie Sie Fakes und Mocks in Ihren Android-Kotlin-Apps verwenden. Wichtig ist insbesondere:

  • Welche Tests Sie für Ihre App implementieren, hängt davon ab, was Sie testen möchten, und von Ihrer Teststrategie. Unittests sind fokussiert und schnell. Integrationstests prüfen die Interaktion zwischen Teilen Ihres Programms. End-to-End-Tests überprüfen Funktionen, haben die höchste Genauigkeit, werden oft instrumentiert und können länger dauern.
  • Die Architektur Ihrer App beeinflusst, wie schwierig es ist, sie zu testen.
  • TDD oder Test-Driven Development ist eine Strategie, bei der Sie zuerst die Tests schreiben und dann die Funktion erstellen, damit die Tests bestanden werden.
  • Um Teile Ihrer App für Tests zu isolieren, können Sie Test-Doubles verwenden. Ein Test-Double ist eine Version einer Klasse, die speziell für Tests entwickelt wurde. Sie geben beispielsweise vor, Daten aus einer Datenbank oder dem Internet zu erhalten.
  • Verwenden Sie Dependency Injection, um eine echte Klasse durch eine Testklasse zu ersetzen, z. B. ein Repository oder eine Netzwerkschicht.
  • Verwenden Sie instrumentierte Tests (androidTest), um UI-Komponenten zu starten.
  • Wenn Sie die Constructor-Dependency-Injection nicht verwenden können, z. B. zum Starten eines Fragments, können Sie häufig einen Service Locator verwenden. Das Service Locator-Muster ist eine Alternative zur Abhängigkeitsinjektion. Dazu wird eine Singleton-Klasse namens „Service Locator“ erstellt, die sowohl für den regulären als auch für den Testcode Abhängigkeiten bereitstellt.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Videos:

Sonstiges:

Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Codelabs zum Thema „Android für Fortgeschrittene mit Kotlin“.