Wprowadzenie do podwójnego testowania i wstrzykiwania zależności

To ćwiczenie programowania jest częścią kursu „Android dla zaawansowanych w Kotlinie”. Korzyści z tego kursu będą dla Ciebie najbardziej wartościowe, jeśli wykonasz je w sekwencjach ćwiczeń z programowania, ale nie jest to obowiązkowe. Wszystkie ćwiczenia z kursu są wymienione na stronie Zaawansowane ćwiczenia z programowania na Androida w Kotlin.

Wstęp

To drugie ćwiczenie ćwiczeń z testu, które dotyczy m.in. testowania wersji na Androida oraz sposobu ich wdrażania za pomocą wstrzykiwania zależności, wzorca Lokalizatora usług i bibliotek. Dzięki temu nauczysz się pisać:

  • Testowanie jednostek repozytorium
  • Testy integracji fragmentów i widoków modeli
  • Testy nawigacji po fragmentach

Co musisz wiedzieć

Pamiętaj:

Czego się nauczysz

  • Jak zaplanować strategię testowania
  • Tworzenie i stosowanie podwójnych testów, tzw. fałszywych próbek i przykładów
  • Ręczne wstrzykiwanie zależności w Androidzie do testów jednostkowych i integracji
  • Jak zastosować wzorzec lokalizatora usług
  • Testowanie repozytoriów, fragmentów, modeli i komponentu Nawigacja

Będziesz używać tych bibliotek i koncepcji kodu:

Jakie zadania wykonasz:

  • Zapis testów jednostkowych w repozytorium przy pomocy podwójnego testu i wstrzyknięcia zależności.
  • Napisz testy jednostkowe w modelu widoku danych, korzystając z testowego podwójnego wstrzykiwania i wstrzyknięcia zależności.
  • Napisz testy integracji fragmentów i ich modeli widoku danych, korzystając z platformy do testowania interfejsu użytkownika Espresso.
  • Pisanie testów nawigacji za pomocą Mockito i Espresso.

W ramach tej serii ćwiczeń z programowania wykonasz kilka zadań w aplikacji Zadania do wykonania. Ta aplikacja pozwoli Ci zapisywać zadania do wykonania i wyświetlać je na liście. Następnie możesz oznaczyć je jako ukończone, odfiltrować lub usunąć.

Ta aplikacja została napisana w Kotlin i ma kilka ekranów, używa komponentów Jetpack oraz jest zgodna ze architekturą przewodnika po architekturze aplikacji. Naucząc się testować tę aplikację, będziesz mieć możliwość testowania aplikacji korzystających z tych samych bibliotek i architektury.

Pobieranie kodu

Na początek pobierz kod:

Pobierz aplikację Zip

Możesz też skopiować kod GitHuba:

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

Poświęć chwilę na zapoznanie się z kodem, wykonując podane niżej instrukcje.

Krok 1. Uruchom przykładową aplikację

Po pobraniu aplikacji Do zrobienia otwórz ją w Android Studio i uruchom. Powinien się skompilować. Poznaj aplikację, wykonując te czynności:

  • Utwórz nowe zadanie z pływającym przyciskiem czynności plus. Wpisz tytuł, a następnie dodatkowe informacje o zadaniu. Zapisz przy użyciu zielonego przycisku wyboru.
  • Na liście zadań kliknij tytuł właśnie zakończonego zadania i wyświetl jego ekran szczegółów, aby wyświetlić pozostałe opisy.
  • Na liście lub na ekranie szczegółów zaznacz pole wyboru obok zadania, by ustawić jego stan na Ukończone.
  • Wróć do ekranu zadań, otwórz menu filtra i przefiltruj zadania według stanu Aktywne lub Ukończone.
  • Otwórz panel nawigacji i kliknij Statystyki.
  • Wróć na ekran przeglądu i z menu panelu nawigacji wybierz Wyczyść ukończone, aby usunąć wszystkie zadania o stanie Ukończone.

Krok 2. Sprawdź przykładowy kod aplikacji

Aplikacja Do zrobienia wykorzystuje przykłady z popularnych testów i architektury architektury Blueprints (korzystając z próbki architektury reaktywnej). Ta aplikacja jest zgodna z architekturą Przewodnika po architekturze aplikacji. Wykorzystuje ono obiekty ViewModels z fragmentami, repozytorium i salą. Jeśli znasz dowolny z tych przykładów, aplikacja ma podobną architekturę:

Ważne jest, aby lepiej zrozumieć ogólną architekturę aplikacji niż zrozumieć jej działanie na jednej warstwie.

Podsumowanie przesyłek znajdziesz tutaj:

Przesyłka: com.example.android.architecture.blueprints.todoapp

.addedittask

Dodawanie lub edytowanie ekranu zadania: kod warstwy interfejsu służący do dodawania i edytowania zadań.

.data

Warstwa danych: dotyczy warstwy danych z zadaniami. Zawiera on bazę danych, sieć i kod repozytorium.

.statistics

Ekran statystyk: kod warstwy interfejsu wyświetlany na ekranie statystyk.

.taskdetail

Ekran szczegółów zadania: kod warstwy interfejsu dotyczący pojedynczego zadania.

.tasks

Ekran Listy zadań: kod warstwy interfejsu zawierający listę wszystkich zadań.

.util

Klasy narzędzi: udostępniane klasy w różnych częściach aplikacji, np. do układu przesuwania używanego na wielu ekranach.

Warstwa danych (.data)

Ta aplikacja zawiera symulowaną warstwę sieciową w pakiecie remote i warstwę bazy danych w pakiecie local. Dla uproszczenia w tym projekcie symulacja warstwy sieciowej jest symulowana za pomocą tylko HashMap z opóźnieniem, a nie przez rzeczywiste żądania sieciowe.

DefaultTasksRepository koordynuje lub pośredniczy między warstwą sieciową a warstwą bazy danych i zwraca dane do warstwy interfejsu.

Warstwa UI ( .addedittask, .statistics, .taskdetail, .tasks)

Każdy pakiet warstw interfejsu zawiera fragment i model widoku, a także wszystkie inne klasy wymagane w interfejsie (takie jak adapter do listy zadań). TaskActivity to działanie zawierające wszystkie fragmenty.

Nawigacja

Nawigacja w aplikacji jest sterowana za pomocą komponentu Nawigacja. Jest on zdefiniowany w pliku nav_graph.xml. Nawigacja jest uruchamiana w modelach widoku danych za pomocą klasy Event. Modele widoku danych określają też, jakie argumenty należy przekazać. Fragmenty śledzą Event i przeprowadzają rzeczywistą nawigację między ekranami.

Z tego modułu dowiesz się, jak testować repozytoria, wyświetlać modele i fragmenty za pomocą podwójnego testowania i wstrzykiwania zależności. Zanim zagłębimy się w te zagadnienia, zastanów się, dlaczego i w jaki sposób będziesz je przygotowywać.

W tej sekcji znajdziesz ogólne sprawdzone metody testowania, które mają zastosowanie do Androida.

Piramida testowa

Omawiając strategię testowania, musisz brać pod uwagę trzy kwestie związane z testowaniem:

  • Zakres – jak duża część kodu jest objęta testem? Testy mogą dotyczyć pojedynczej metody, całej aplikacji lub pośrednio.
  • Szybkość – jak szybko działa test? Szybkość testu może być różna od kilku milisekund do kilku minut.
  • Dokładność – jak jest w rzeczywistości „test”? Na przykład, jeśli część kodu jest testowana jako żądanie sieci, czy kod testowy faktycznie wysyła to żądanie sieciowe, czy może fałszywy wynik? Jeśli test rzeczywiście komunikuje się z siecią, oznacza to, że ma wyższą wierność. Wadą jest to, że test może potrwać dłużej, może powodować błędy, jeśli sieć przestanie działać lub może być kosztowna.

Między tymi aspektami niosą ze sobą pewne kompromisy. Na przykład szybkość i wierność stanowią kompromis – im szybciej testujesz, tym niższa jest wierność i odwrotnie. Testy automatyczne można podzielić na 3 kategorie:

  • Testy jednostkowe – bardzo precyzyjne testy, które mają miejsce w przypadku jednych zajęć, a jedynie pojedynczej metody. Jeśli test jednostkowy nie powiedzie się, będziesz dokładnie wiedzieć, gdzie znajduje się problem w kodzie. Mają one niską wierność, ponieważ w prawdziwym świecie aplikacja wymaga znacznie więcej niż tylko wykonanie jednej metody lub klasy. Są one wystarczająco szybkie, aby można było je uruchamiać po każdej zmianie kodu. Testy będą przeprowadzane najczęściej lokalnie (w zbiorze źródłowym test). Przykład: testowanie pojedynczych metod w modelach i repozytoriach widoku danych.
  • Testy integracji – testują interakcję kilku klas, aby sprawdzić, czy działają razem ze sobą. Jeśli chcesz zorganizować testy integracji, poproś ich o przetestowanie jednej funkcji, na przykład możliwości zapisania zadania. Testują one większy zakres kodu niż testy jednostkowe, ale są zoptymalizowane pod kątem szybkiego działania w porównaniu z pełną wiernością. Zależnie od sytuacji mogą być przeprowadzane lokalnie lub jako testy instrumentacyjne. Przykład: testowanie wszystkich funkcji pojedynczego fragmentu i pary modeli.
  • Testy kompleksowe (E2e) – przetestuj kombinację współdziałających funkcji. Testują duże części aplikacji, dokładnie symulują rzeczywiste użycie, dlatego zwykle są one powolne. Mają największą wierność i informują, że aplikacja faktycznie działa w całości. Testy te będą w dużym stopniu dotyczyć instrumentów (w zbiorze źródłowym: androidTest)
    Przykład: uruchamianie całej aplikacji i testowanie kilku funkcji razem.

Sugerowana część tych testów jest często reprezentowana przez piramidę. Większość testów to testy jednostkowe.

Architektura i testy

Możliwość testowania aplikacji na różnych poziomach piramidy testowej jest z natury związana z Twoją architekturą aplikacji. Na przykład doskonała architektura może spowodować, że cała logika aplikacji zostanie umieszczona w jednej metodzie. W takim przypadku możesz napisać kompleksowy test, bo zwykle pozwala on na testowanie dużej części aplikacji. Ale co z testami jednostek lub integracji? Mając wszystkie kody w jednym miejscu, trudno jest przetestować tylko kod związany z jedną jednostką lub funkcją.

Lepszym sposobem jest podzielenie logiki aplikacji na kilka metod i klas, dzięki czemu każdy element będzie można przetestować w izolacji. Architektura to sposób na podział i porządkowanie kodu, który ułatwia testowanie jednostek i integracji. Aplikacja Do zrobienia, którą będziesz testować, działa w oparciu o konkretną architekturę:



Z tej lekcji dowiesz się, jak przetestować poszczególne części powyższej architektury z odpowiednią izolacją:

  1. Najpierw przetestuj jednostkę w repozytorium.
  2. Następnie w modelu widoku danych użyjesz testowego eksperymentu, który jest niezbędny na potrzeby testowania jednostkowego i testowania integracji z modelem widoku.
  3. Następnie nauczysz się pisać testy integracji dla fragmentów i ich modeli widoku danych.
  4. Na koniec nauczysz się pisać testy integracji, które obejmują komponent nawigacji.

Pełne testy zostaną omówione w następnej lekcji.

Gdy piszesz test jednostkowy części klasy (metody lub niewielkiej kolekcji metod), Twoim celem jest testowanie kodu tylko na tych zajęciach.

Testowanie tylko określonych zajęć lub zajęć może być trudne. Przeanalizujmy poniższy przykład. Otwórz klasę data.source.DefaultTaskRepository w zbiorze źródłowym main. To jest repozytorium aplikacji, w którym będziesz zapisywać testy jednostek.

Celem jest przetestowanie tylko kodu zajęć. Jednak DefaultTaskRepository zależy od działania innych klas, takich jak LocalTaskDataSource i RemoteTaskDataSource. Innym sposobem jest to, że LocalTaskDataSource i RemoteTaskDataSource to zależności DefaultTaskRepository.

Zatem każda metoda z metod DefaultTaskRepository w klasach źródła danych, która z kolei używa innych metod wywołania z innych klas, aby zapisać informacje w bazie danych lub komunikować się z siecią.



Spójrzmy na przykład na tę metodę w aplikacji DefaultTasksRepo.

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

getTasks to jedno z najczęstszych "podstawowych wywołań Twojego repozytorium. Ta metoda obejmuje odczyt z bazy danych SQLite i wykonywanie wywołań sieciowych (wywołanie updateTasksFromRemoteDataSource). Wymaga to znacznie więcej kodu niż tylko kodu repozytorium.

Oto kilka konkretnych powodów, dla których testowanie repozytorium jest trudne:

  • Aby móc utworzyć proste repozytorium dla tego repozytorium, musisz zająć się tworzeniem bazy danych i zarządzaniem nią. Pojawia się pytanie, czy powinien to być test lokalny czy zinstrumentowany. Jeśli chcesz przeprowadzić symulację środowiska Androida, użyj testu X.
  • Uruchomienie niektórych elementów kodu, np. kodu sieciowego, może zająć dużo czasu, a czasami się nie udać, przez co długie testy działają niestabilnie.
  • Twoje testy mogą stracić możliwość zdiagnozowania, który kod jest przyczyną błędu. Testy mogą rozpocząć testowanie kodu spoza repozytorium, więc na przykład Twoje domniemane „repozytorium” mogą się nie powieść z powodu błędu w niektórym kodzie zależnym, takim jak kod bazy danych.

Dwójki

Rozwiązaniem tego problemu jest to, że podczas testowania repozytorium nie używaj prawdziwego kodu sieci ani kodu bazy danych, ale zamiast tego używaj podwójnego testu. Podwójna wersja testowa to wersja klasy przeznaczona do testowania. Ma na celu zastąpienie rzeczywistej wersji zajęć. Można to porównać do akrobacji, która jest aktorem, który specjalizuje się w wyczynach kaskaderskich, i zastępuje faktyczną postać niebezpieczną.

Oto kilka rodzajów testowych testów:

Fałszywy

Dwuelementowy test z wdrożeniem klasy i implementacją kodu w sposób, który sprawdza się w przypadku testów, ale nie nadaje się do produkcji.

Pozorowanie

Podwójny test śledzący, które z metod zostały wywołane. Test jest zaliczony lub nieskuteczny w zależności od tego, czy zostały prawidłowo wywołane.

Szczyt

Podwójny test, bez żadnej logiki, i zwraca tylko to, co zaprogramujesz do zwrócenia. Zaprogramowanie StubTaskRepository może być na przykład programowane do zwracania określonych kombinacji zadań z getTasks.

Lalka

Testowa wersja podwójna, która jest przekazywana, ale nie jest używana, np. gdy trzeba przekazać ją jako parametr. Jeśli masz kod NoOpTaskRepository, musi on zaimplementować TaskRepository kod no w żadnej z tych metod.

Szpiegostwo

Podwójny test pozwala też śledzić dodatkowe informacje. Jeśli np. wywołałeś funkcję SpyTaskRepository, może ona śledzić liczbę wywołań metody addTask.

Więcej informacji na temat podwójnego testu znajdziesz w artykule Test on the Toilet: Know Your Test Doubles.

Najczęściej występujące w Androidzie podwójne wartości to Faks i Makiety.

W tym zadaniu utworzysz FakeDataSource test podwójny, aby przeprowadzić test jednostkowy DefaultTasksRepository, który będzie oddzielony od rzeczywistych źródeł danych.

Krok 1. Utwórz klasę FakeDataSource

W tym kroku utworzysz klasę o nazwie FakeDataSouce, która będzie podwójną próbką wartości LocalDataSource i RemoteDataSource.

  1. W zestawie źródłowym testowym kliknij prawym przyciskiem myszy Nowy -> Pakiet.

  1. Utwórz pakiet danych z pakietem źródłowym.
  2. Utwórz nową klasę o nazwie FakeDataSource w pakiecie data/source.

Krok 2. Zaimplementuj interfejs TasksDataSource

Aby móc używać nowej klasy FakeDataSource jako testu testowego, musisz w nim zastąpić inne źródła danych. Te źródła danych to TasksLocalDataSource i TasksRemoteDataSource.

  1. Zwróć uwagę, jak w obu przypadkach wdrożono interfejs TasksDataSource.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Skonfiguruj FakeDataSource: TasksDataSource
class FakeDataSource : TasksDataSource {

}

Android Studio zgłosi, że nie masz zastosowanych wymaganych metod dla interfejsu TasksDataSource.

  1. Użyj menu szybkiej naprawy i wybierz Wdróż członków.


  1. Wybierz wszystkie metody i naciśnij OK.

Krok 3. Zaimplementuj metodę getTasks w FakeDataSource

FakeDataSource to szczególny rodzaj testu, zwany fake. Fałszywy materiał to tzw. podwójny „test”, który jest zaimplementowany z zastosowaniem klasy, ale jest zaimplementowany w sposób, który sprawdza się podczas testów, ale nie nadaje się do produkcji. Zastosowanie &parametru pracy oznacza, że przy tworzeniu danych wejściowych klasy będą miały realistyczne wyniki.

Na przykład fałszywe źródło danych nie połączy się z siecią ani nie zapisze niczego w bazie danych. Zostanie użyte tylko listy zapisane w pamięci. Będzie to działać w oczekiwany sposób zgodnie z oczekiwaniami i zapisaniem zadań w wersji produkcyjnej, ale nigdy nie można używać tej implementacji w wersji produkcyjnej, ponieważ nie jest ona zapisywana na serwerze ani w bazie danych.

FakeDataSource

  • pozwala przetestować kod w systemie DefaultTasksRepository bez konieczności korzystania z prawdziwej bazy danych ani sieci.
  • zapewnia „implementację do testowania”.
  1. Zmień konstruktor FakeDataSource, aby utworzyć var o nazwie tasks o wartości MutableList<Task>? i wartości domyślnej z pustą listą zmiennych.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Jest to lista zadań, które są „bazą danych” lub odpowiedzią serwera. Na razie celem jest przetestowanie metody repozytorium getTasks. Wywołuje on metody źródło danych getTasks, deleteAllTasks i saveTask.

Fałszywa wersja tych metod:

  1. Napisz getTasks: jeśli tasks nie jest null, zwróć wynik Success. Jeśli tasks ma wartość null, zwróć wynik Error.
  2. Wpisz deleteAllTasks: wyczyść listę zadań.
  3. Wpisz saveTask: dodaj zadanie do listy.

Te metody, zaimplementowane na koncie FakeDataSource, wyglądają tak jak w poniższym kodzie.

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

W razie potrzeby możesz zaimportować te instrukcje importu:

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

Przypomina to działanie lokalnego i zdalnego źródła danych.

W tym kroku wykorzystasz technikę zwaną wstrzykiwaniem zależności ręcznej, która pozwala użyć fałszywego, podwójnego testu.

Główny problem polega na tym, że masz atrybut FakeDataSource, ale nie jest on przejrzysty, jak chcesz go używać w testach. Ciąg TasksRemoteDataSource musi zostać zastąpiony elementem TasksLocalDataSource, ale tylko w testach. Zarówno TasksRemoteDataSource, jak i TasksLocalDataSource są zależnościami tagu DefaultTasksRepository, co oznacza, że działanie DefaultTasksRepositories wymaga lub zależy od tych klas.

W tej chwili zależności są tworzone w ramach metody init DefaultTasksRepository.

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
}

Ponieważ tworzysz i przypisujesz elementy taskLocalDataSource oraz tasksRemoteDataSource w obrębie DefaultTasksRepository, są one zasadniczo zakodowane na stałe. Podwójnego testu nie można zamienić.

Zamiast tego prześlij te źródła danych na potrzeby klasy, zamiast kodować je na stałe. Udostępnienie zależności jest nazywane wstrzyknięciem zależności. Istnieją różne sposoby na podawanie zależności, a przez to na wstrzykiwanie ich do różnych typów.

Wstrzyknięcie zależności od konstruktora umożliwia podwójne zastąpienie testu przez przekazanie go do konstruktora.

Brak zastrzyków

Wstrzyknięcie

Krok 1. Użyj wstrzykiwania zależności od konstruktora w domyślnym repozytorium zadań

  1. Zmień konstruktor DefaultTaskRepository+ z Application na przechwytywanie zarówno źródeł danych, jak i dyspozytora danych (który trzeba też wymienić na potrzeby testów – więcej informacji na ten temat znajdziesz w trzeciej lekcji na temat współprogramów).

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. Zostały podane zależności, więc usuń metodę init. Nie musisz już tworzyć zależności.
  2. Usuń też stare zmienne instancji. Definiujesz je w konstruktorze:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Na koniec zaktualizuj metodę getRepository, by używała nowego konstruktora:

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

Używasz teraz wstrzyknięcia zależności konstruktora.

Krok 2. Użyj FakeDataSource w testach

Teraz, gdy Twój kod korzysta z wstrzyknięcia zależności konstruktora, możesz użyć fałszywego źródła danych, by przetestować DefaultTasksRepository.

  1. Kliknij prawym przyciskiem myszy nazwę klasy DefaultTasksRepository i wybierz Wygeneruj, a następnie Przetestuj.
  2. Postępuj zgodnie z wyświetlanymi instrukcjami, aby utworzyć zbiór DefaultTasksRepositoryTest w zestawie źródłowym testu.
  3. U góry nowej klasy DefaultTasksRepositoryTest dodaj zmienne użytkownika poniżej, aby reprezentować dane w fałszywych źródłach danych.

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. Utwórz 3 zmienne, 2 zmienne o członku FakeDataSource (po 1 na każde źródło danych w repozytorium) i zmienną dla zmiennej DefaultTasksRepository, którą przetestujesz.

DefaultTasksRepositoryTest.kt

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

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Utwórz metodę inicjowania i inicjowania DefaultTasksRepository. Ten DefaultTasksRepository użyje Twojego drugiego testu (FakeDataSource).

  1. Utwórz metodę o nazwie createRepository i dodaj do niej komentarz za pomocą atrybutu @Before.
  2. Utwórz wystąpienia fałszywych źródeł danych za pomocą list remoteTasks i localTasks.
  3. Utwórz instancję tasksRepository przy użyciu 2 sfałszowanych źródeł danych i Dispatchers.Unconfined.

Ostateczna metoda powinna przypominać poniższy kod.

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

Krok 3. Zapisz test DefaultTasksRepository getTasks()

Czas napisać test DefaultTasksRepository!

  1. Napisz test dla metody getTasks repozytorium. Upewnij się, że wywołanie getTasks z adresem true (ponowne załadowanie danych ze zdalnego źródła danych) zwraca dane z zdalnego źródła danych (a nie lokalnego źródła danych).

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

Podczas rozmowy getTasks: pojawi się błąd

Krok 4. Dodaj test RunBlokowanie

Oczekiwany błąd współzawodnika jest spowodowany funkcją getTasks w funkcji suspend. Aby ją wywołać, musisz ją uruchomić. Aby to zrobić, musisz mieć zakres ograniczony. Aby rozwiązać ten problem, musisz dodać pewne zależności Gradle do obsługi uruchamianych algorytmów.

  1. Dodaj wymagane zależności do testów w kodzie źródłowym za pomocą narzędzia testImplementation.

app/build.gradle

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

Nie zapomnij zsynchronizować!

kotlinx-coroutines-test to biblioteka testu współprogramów, przeznaczona specjalnie do testowania współprogramów. Aby przeprowadzić testy, użyj funkcji runBlockingTest. Ta funkcja jest dostępna w bibliotece testowej współprogramów. Składa się on z bloku kodu, a następnie uruchamia go w specjalnym kontekście, który działa synchronicznie i od razu. To oznacza, że działania są wykonywane w określonej kolejności. To w zasadzie oznacza, że Twoja współorganizacja działa jak niekoordynerzy, więc jest przeznaczona do testowania kodu.

Podczas wywoływania funkcji suspend używaj klasy runBlockingTest w klasach testowych. Z następnego ćwiczenia z serii dowiesz się więcej o działaniu usługi runBlockingTest i o sposobie testowania algorytmów.

  1. Dodaj @ExperimentalCoroutinesApi nad zajęciami. Potwierdza to, że wiesz, że w ramach zajęć korzystasz z eksperymentalnego interfejsu API Corordine (runBlockingTest). Jeśli tego nie zrobisz, otrzymasz ostrzeżenie.
  2. Wróć do DefaultTasksRepositoryTest, dodaj runBlockingTest, aby umożliwić cały test jako „&blockt” kodu

Ostatni test wygląda jak poniższy kod.

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. Uruchom nowy test getTasks_requestsAllTasksFromRemoteDataSource i upewnij się, że działa, a błąd zniknął.

Wiesz już, jak jednorazowo przetestować testowanie repozytorium. W kolejnych krokach ponownie użyjesz wstrzykiwania zależności i utworzysz kolejną próbę testową – tym razem, aby pokazać, jak zapisywać testy jednostkowe i integrację na potrzeby modeli widoku danych.

Testy jednostkowe powinny testować tylko te zajęcia lub metody, które Cię interesują. Jest to tzw. testowanie na izolacji, w której wyraźnie wyodrębniasz jednostkę i testujesz tylko kod, który jest częścią tej jednostki.

Aplikacja TasksViewModelTest powinna więc testować tylko kod TasksViewModel, a nie klasy bazy danych, sieci czy repozytorium. Dlatego w przypadku modeli widoku danych, podobnie jak w przypadku repozytorium, utworzysz fałszywe repozytorium i zastosujesz wstrzykiwanie zależności, które będzie wykorzystywane w testach.

W tym zadaniu stosujesz wstrzykiwanie zależności, aby wyświetlać modele.

Krok 1. Tworzenie interfejsu repozytorium zadań

Pierwszym krokiem do wstrzyknięcia zależności od konstruktora jest utworzenie wspólnego interfejsu wspólnego między fałszywą a prawdziwą klasą.

Jak to wygląda w praktyce? Przyjrzyjmy się funkcjom TasksRemoteDataSource, TasksLocalDataSource i FakeDataSource. Zauważ, że wszyscy mają ten sam interfejs: TasksDataSource. Pozwoli to powiedzieć w konstruktorze DefaultTasksRepository, że obiekt TasksDataSource jest w konstruktorze.

DefaultTasksRepository.kt

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

W ten sposób możemy wymienić Twoje urządzenie FakeDataSource.

Następnie utwórz interfejs usługi DefaultTasksRepository – tak jak w przypadku źródeł danych. Musi zawierać wszystkie publiczne metody (publiczny interfejs API) usługi DefaultTasksRepository.

  1. Otwórz DefaultTasksRepository i kliknij prawym przyciskiem myszy nazwę zajęć. Następnie wybierz Refaktor -> Wyodrębnij -> Interfejs.

  1. Wybierz Rozpakuj, aby oddzielić plik.

  1. W oknie Wyodrębnij interfejs zmień nazwę interfejsu na TasksRepository.
  2. W sekcji Members to form Interface (Członkowie w formularzu) zaznacz wszystkich członków oprócz 2 członków i metod prywatnych.


  1. Kliknij Refaktor. Nowy interfejs TasksRepository powinien pojawić się w pakiecie data/source.

Natomiast DefaultTasksRepository korzysta teraz z TasksRepository.

  1. Uruchom aplikację (a nie testy), aby upewnić się, że wszystko działa prawidłowo.

Krok 2. Tworzenie fałszywego repozytorium testowego

Gdy masz już interfejs, możesz utworzyć podwójny test DefaultTaskRepository.

  1. W zbiorze danych test w data/source utwórz plik Kotlin i klasę FakeTestRepository.kt, a następnie rozwiń interfejs z TasksRepository.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Wyświetli się komunikat, że musisz wdrożyć metody interfejsu.

  1. Najedź kursorem na błąd, aż pojawi się menu sugestii, a potem kliknij i wybierz Wdróż członków.
  1. Wybierz wszystkie metody i naciśnij OK.

Krok 3. Wdrażanie fałszywych metod w repozytorium testów

Masz teraz klasę FakeTestRepository z metodami „niezaimplementowane”. Podobnie jak w przypadku wdrożenia obiektu FakeDataSource dane w narzędziu FakeTestRepository będą oparte na strukturze danych, dzięki czemu nie trzeba będzie wykonywać skomplikowanego zapośredniczenia między lokalnymi i zdlnymi źródłami danych.

Pamiętaj, że FakeTestRepository nie musi używać zmiennych FakeDataSource ani podobnych. Musi po prostu zwracać realistyczne dane wyjściowe na podstawie podanych danych. Użyj LinkedHashMap do przechowywania listy zadań, a MutableLiveData dla zadań do obserwowania.

  1. W FakeTestRepository dodaj zmienną LinkedHashMap reprezentującą bieżącą listę zadań i MutableLiveData dla zadań do obserwowania.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

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

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


    // Rest of class
}

Zaimplementuj te metody:

  1. getTasks – powinna użyć metody tasksServiceData i zmienić ją w listę przy użyciu tasksServiceData.values.toList(), a następnie zwrócić jako wynik Success.
  2. refreshTasks – aktualizuje wartość observableTasks w taki sposób, aby była zwracana przez funkcję getTasks().
  3. observeTasks – tworzy regułę przy użyciu runBlocking i uruchamia refreshTasks, a potem zwraca observableTasks.

Poniżej znajdziesz kod poszczególnych metod.

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

}

Krok 4. Dodaj metodę testowania do addTasks

Podczas testowania lepiej mieć część Tasks w repozytorium. Możesz wywołać funkcję saveTask kilka razy, ale dla ułatwienia dodaj metodę pomocniczą specjalnie do testów, która pozwala dodawać zadania.

  1. Dodaj metodę addTasks, która pobiera vararg zadań, dodaj każde z nich do HashMap, a następnie odświeżasz zadania.

FakeTestRepository.kt

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

Istnieje już fałszywe repozytorium do testowania z kilkoma kluczowymi metodami implementacji. Następnie wykorzystaj go do testów.

W tym zadaniu używasz fałszywej klasy wewnątrz ViewModel. Dzięki wstrzykiwaniu zależności konstruktora stosuje się wstrzykiwanie zależności do dwóch źródeł danych, dodając zmienną TasksRepository do konstruktora TasksViewModel.

Ten proces wygląda nieco inaczej w przypadku modeli widoku danych, ponieważ nie są one tworzone bezpośrednio. Przykład:

class TasksFragment : Fragment() {

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

}


Jak w powyższym kodzie, używasz modelu przekazywania dostępu do viewModel's, który tworzy model widoku. Aby zmienić sposób tworzenia modelu widoku, musisz dodać i używać obiektu ViewModelProvider.Factory. Jeśli nie znasz jeszcze ViewModelProvider.Factory, tutaj dowiesz się więcej na ten temat.

Krok 1. Tworzenie i używanie elementu ViewModelFactory w ListViewModel

Najpierw aktualizujesz zajęcia i testy związane z ekranem Tasks.

  1. Otwórz TasksViewModel.
  2. Zmień konstruktor TasksViewModel, aby pobierał element TasksRepository, zamiast tworzyć go w ramach klasy.

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

Ponieważ został przez Ciebie zmieniony konstruktor, musisz teraz utworzyć fabrykę TasksViewModel, aby utworzyć konstrukcję. Umieść klasę fabryki w tym samym pliku co atrybut TasksViewModel, ale możesz go też umieścić w osobnym pliku.

  1. Na dole pliku TasksViewModel poza zajęciami dodaj właściwość TasksViewModelFactory w standardowym formacie TasksRepository.

TaskTaskViewModel.kt

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


Jest to standardowa metoda zmiany sposobu tworzenia elementów ViewModel. Po utworzeniu fabryki możesz używać go przy tworzeniu modelu widoku.

  1. Aby korzystać z fabryki, zaktualizuj aplikację TasksFragment.

TasksFragment.kt

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

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Uruchom kod aplikacji i sprawdź, czy wszystko działa.

Krok 2. Używanie FakeTestRepository wewnątrz TasksViewModelTest

Teraz zamiast używać prawdziwego repozytorium w testach modelu widoku możesz użyć fałszywego repozytorium.

  1. Otwórz aplikację TasksViewModelTest.
  2. Dodaj właściwość FakeTestRepository w elemencie TasksViewModelTest.

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. Zaktualizuj metodę setupViewModel, by utworzyć FakeTestRepository z 3 zadaniami, a następnie utwórz tasksViewModel za pomocą tego repozytorium.

TaskTaskViewModelTest.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. Nie korzystasz już z kodu AndroidX Test ApplicationProvider.getApplicationContext, dlatego możesz też usunąć adnotację @RunWith(AndroidJUnit4::class).
  2. Uruchom testy, aby upewnić się, że nadal działają.

Dzięki wstrzykiwaniu zależności konstruktora udało Ci się usunąć DefaultTasksRepository jako zależność, a w testach zastąpić go elementem FakeTestRepository.

Krok 3. Zaktualizuj też fragment TaskDetails i ViewModel

Wprowadź dokładnie te same zmiany w kolumnach TaskDetailFragment i TaskDetailViewModel. Spowoduje to przygotowanie kodu do napisania kolejnych testów (TaskDetail).

  1. Otwórz TaskDetailViewModel.
  2. Zaktualizuj konstruktor:

TaskDetailsViewModel.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. Na dole pliku TaskDetailViewModel poza klasą dodaj TaskDetailViewModelFactory.

TaskDetailsViewModel.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. Aby korzystać z fabryki, zaktualizuj aplikację TasksFragment.

TasksFragment.kt

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

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Uruchom kod i sprawdź, czy wszystko działa.

Teraz możesz używać FakeTestRepository zamiast faktycznego repozytorium w TasksFragment i TasksDetailFragment.

Następnie napisz testy integracji, aby przetestować interakcje fragmentu i modelu widoku danych. Dowiesz się, czy Twój kod modelu widoku danych odpowiednio aktualizuje interfejs. Służy do tego

  • Wzorzec ServiceLocator
  • Biblioteki espresso i Mockito

Testy integracji służą do testowania interakcji kilku klas i sprawdzania, czy działają one zgodnie z oczekiwaniami. Testy mogą być przeprowadzane lokalnie (test zbiór źródłowy) lub jako instrumentacja (androidTest zbiór źródła).

W Twoim przypadku trzeba zrobić każdy fragment i napisać testy integracji fragmentu i widoku, aby przetestować jego główne funkcje.

Krok 1. Dodaj zależności Gradle

  1. Dodaj te zależności Gradle.

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"

Oto niektóre z nich:

  • junit:junit – JUnit, który jest niezbędny do pisania podstawowych instrukcji testowych;
  • androidx.test:core – podstawowa biblioteka testowa Androida X
  • kotlinx-coroutines-test – biblioteka testów współprogramów.
  • androidx.fragment:fragment-testing – biblioteka testowa AndroidaX do tworzenia fragmentów testów i zmiany ich stanu.

Ponieważ będziesz używać tych bibliotek w zestawie źródłowym w usłudze androidTest, dodaj je jako zależności za pomocą zasady androidTestImplementation.

Krok 2. Tworzenie klasy TaskDetailsFragmentTest

Pole TaskDetailFragment zawiera informacje o 1 zadaniu.

Najpierw trzeba przeprowadzić test fragmentu TaskDetailFragment, ponieważ ma on dość podstawową funkcję w porównaniu z innymi fragmentami.

  1. Otwórz taskdetail.TaskDetailFragment.
  2. Wygeneruj test TaskDetailFragment, tak jak wcześniej. Zaakceptuj ustawienia domyślne i umieść je w zestawie źródłowym androidTest (a nie w źródle źródłowym test).

  1. Dodaj następujące adnotacje do klasy TaskDetailFragmentTest.

TaskDetailsFragmentTest.kt

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

}

Uwaga:

Krok 3. Uruchamianie fragmentu z testu

W tym zadaniu uruchomisz TaskDetailFragment za pomocą biblioteki testowania na Androida. FragmentScenario to klasa testu Androida X, która obejmuje fragment i daje Ci bezpośrednią kontrolę nad cyklem życia tego fragmentu w testach. Aby napisać testy fragmentów, utwórz FragmentScenario dla tego fragmentu (TaskDetailFragment).

  1. Skopiuj ten test do TaskDetailFragmentTest.

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

    }

Powyższy kod:

To jeszcze nie koniec, ponieważ żaden wynik nie jest potwierdzony. Na razie przeprowadź test i sprawdź, co się stanie.

  1. Jest to test z instrumentacją, więc upewnij się, że emulator lub urządzenie jest widoczne.
  2. Uruchom test.

Kilka rzeczy powinno się wydarzyć.

  • Po pierwsze, ponieważ jest to test z instrumentacją, przeprowadzany na fizycznym urządzeniu (jeśli jest podłączony) lub w emulatorze.
  • Powinien uruchomić się ten fragment.
  • Zwróć uwagę, że nie porusza się ona po innych fragmentach ani nie ma żadnych menu powiązanych z aktywnością – to tylko fragment.

Na koniec sprawdź, czy fragment brzmi „Brak danych”, bo nie wczytuje danych zadania.

Podczas testu konieczne jest wczytanie danych TaskDetailFragment (ukończono już) i sprawdzenie, czy dane zostały prawidłowo wczytane. Dlaczego nie ma danych? Wynika to z tego, że zadanie zostało utworzone, ale nie zostało zapisane w repozytorium.

    @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)

    }

Masz taki zasób FakeTestRepository, ale musisz znaleźć sposób na zastąpienie swojego prawdziwego repozytorium repliką z fragmentem. Zrób to teraz!

W tym zadaniu dostarczysz fałszywe repozytorium fragmentu, używając ServiceLocator. W ten sposób możesz zapisać fragment i wyświetlić testy integracji modelu.

Nie możesz tu wstrzyknąć zależności zależności Konstruktor, tak jak wcześniej, gdy konieczne było określenie zależności dla modelu widoku lub repozytorium. Wstrzyknięcie zależności Konstruktor wymaga skonstruowania klasy. Fragmenty i aktywności to przykłady klas, których nie stworzysz i które zwykle nie mają dostępu do konstruktora.

Nie możesz utworzyć fragmentu, dlatego nie możesz wstrzyknąć zależności zależności od konstruktora, aby zamienić test z repozytorium dwukrotnie (FakeTestRepository) na odpowiedni fragment. Zamiast tego użyj wzorca Lokalizator usług. Wzorzec lokalizatora usług jest alternatywą dla wstrzykiwania zależności. Polega ona na utworzeniu klasy pojedynczego typu o nazwie "Lokalizator usług&, który ma na celu podawanie zależności zarówno w standardowym, jak i w kodzie testowym. W zwykłym kodzie aplikacji (zbiór źródłowy main) wszystkie te zależności to zwykłe zależności aplikacji. Na potrzeby testów musisz zmodyfikować lokalizator usług, aby dostarczyć podwójne wersje zależności zależności.

Nie korzystam z Lokalizatora usług


Korzystanie z lokalizatora usług

W przypadku tej aplikacji wykonaj te czynności:

  1. Utwórz klasę Lokalizatora usług, która może skompilować i zapisać repozytorium. Domyślnie kompiluje &repozytorium.
  2. refaktoryzacja kodu tak, aby w przypadku określonego repozytorium używać Lokalizatora usług;
  3. W klasie klasę wywołaj metodę w Lokalizatorze usług, która zastępuje &ret;normal" repozytorium podwójnym testem.

Krok 1. Tworzenie usługi Service Locator

Utwórz klasę ServiceLocator. Będzie znajdować się w głównym zestawie źródłowym wraz z pozostałą częścią kodu aplikacji, ponieważ jest wykorzystywana przez główny kod aplikacji.

Uwaga: element ServiceLocator jest pojedynczy, więc użyj w klasie klasy słowa kluczowego Kotlin object.

  1. Utwórz plik ServiceLocator.kt na najwyższym poziomie głównego zestawu źródeł.
  2. Określ object o nazwie ServiceLocator.
  3. Utwórz zmienne wystąpienia database i repository i ustaw ich wartość na null.
  4. Dodaj do repozytorium adnotacje @Volatile, ponieważ może być ono używane w wielu wątkach (@Volatile szczegółowo opisujemy to tutaj).

Twój kod powinien wyglądać tak jak poniżej.

object ServiceLocator {

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

}

Obecnie ServiceLocator potrzebuje tylko znać, jak zwrócić TasksRepository. Zwróci on istniejący element DefaultTasksRepository lub w razie potrzeby zwróci nowy DefaultTasksRepository.

Zdefiniuj te funkcje:

  1. provideTasksRepository – każde z nich zawiera już utworzone repozytorium lub tworzy nowe. Powinna to być metoda synchronized w systemie this, aby uniknąć sytuacji, w której uruchomiony jest kilka wątków, i przypadkowo przypadkowo zostaną utworzone 2 instancje repozytorium.
  2. createTasksRepository – kod służący do tworzenia nowego repozytorium. Wywoła polecenie createTaskLocalDataSource i utworzy nową grupę: TasksRemoteDataSource.
  3. createTaskLocalDataSource – kod służący do tworzenia nowego lokalnego źródła danych. Zadzwoni do: createDataBase.
  4. createDataBase – kod służący do tworzenia nowej bazy danych.

Pełny kod znajduje się poniżej.

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

Krok 2. Używanie usługi Service Locator w aplikacji

Wprowadzisz zmianę w głównym kodzie aplikacji (a nie w testach) tak, by utworzono repozytorium w jednym miejscu – ServiceLocator.

Ważne jest, aby utworzyć jedną instancję klasy repozytorium. Aby to zrobić, skorzystaj z lokalizatora usług w Twojej klasie aplikacji.

  1. Na najwyższym poziomie hierarchii pakietów otwórz TodoApplication i utwórz val dla swojego repozytorium, a następnie przypisz mu repozytorium otrzymane za pomocą ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

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

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

Repozytorium aplikacji zostało już utworzone, więc możesz usunąć starą metodę getRepository z przeglądarki DefaultTasksRepository.

  1. Otwórz DefaultTasksRepository i usuń obiekt towarzyszący.

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

Teraz w każdym miejscu, w którym był używany getRepository, użyj aplikacji taskRepository. Dzięki temu zamiast tworzyć bezpośrednio repozytorium, pobierasz każde z nich, które udostępnia ServiceLocator.

  1. Otwórz TaskDetailFragement i znajdź połączenie z numerem getRepository u góry klasy.
  2. Zastąp to wywołanie wywołaniem, które pobiera repozytorium z TodoApplication.

TaskDetailsFragment.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. Zrób to samo w przypadku: 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. W przypadku StatisticsViewModel i AddEditTaskViewModel zaktualizuj kod, który pobiera repozytorium, aby używać go z TodoApplication.

TasksFragment.kt

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



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Uruchom aplikację (a nie test)!

Ze względu na faktyczny zwrot środków aplikacja powinna działać tak samo bez problemów.

Krok 3. Tworzenie fałszywego repozytorium AndroidTest

Masz już FakeTestRepository w zestawie testowym źródle. Domyślnie nie można udostępniać klas testowych między zbiorami źródłowymi test i androidTest. Musisz więc utworzyć zduplikowaną klasę FakeTestRepository w zbiorze źródłowym androidTest i wywołać ją FakeAndroidTestRepository.

  1. Kliknij prawym przyciskiem myszy zbiór źródłowy androidTest i utwórz pakiet danych. Ponownie kliknij prawym przyciskiem myszy i utwórz pakiet źródłowy.
  2. Utwórz nową klasę w tym pakiecie źródłowym o nazwie FakeAndroidTestRepository.kt.
  3. Skopiuj następujący kod do tych zajęć.

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

Krok 4. Przygotowywanie usługi Service Locator na potrzeby testów

Czas użyć ServiceLocator do zamiany w teście podczas testowania. Aby to zrobić, musisz dodać część kodu do kodu ServiceLocator.

  1. Otwórz ServiceLocator.kt.
  2. Oznacz metodę ustawiającą tasksRepository jako @VisibleForTesting. Dzięki tej adnotacji możesz potwierdzić, że element ustawiający jest publiczny, ponieważ jest testowany.

ServiceLocator.kt

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

Wszystkie testy powinny być przeprowadzane w taki sam sposób, niezależnie od tego, czy chcesz przeprowadzić je samodzielnie, czy w grupie testów. Oznacza to, że testy nie mogą wykazywać zachowań, które są od siebie zależne (co oznacza unikanie udostępniania obiektów między testami).

Element ServiceLocator jest pojedynczym kablem, dlatego może się zdarzyć, że przypadkowo zostanie udostępniony między testami. Aby temu zapobiec, utwórz metodę, która prawidłowo resetuje stan ServiceLocator między testami.

  1. Dodaj zmienną instancji o nazwie lock z wartością Any.

ServiceLocator.kt

private val lock = Any()
  1. Dodaj metodę związaną z testowaniem o nazwie resetRepository, która czyści bazę danych i ustawia repozytorium oraz bazę danych na wartość null.

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

Krok 5. Korzystanie z lokalizatora usług

Ten krok to: ServiceLocator.

  1. Otwórz TaskDetailFragmentTest.
  2. Zadeklaruj zmienną lateinit TasksRepository.
  3. Dodaj konfigurację i metodę demontażu, aby skonfigurować FakeAndroidTestRepository przed każdym testem i wyczyścić jego pamięć po każdym teście.

TaskDetailsFragmentTest.kt

    private lateinit var repository: TasksRepository

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

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Spakuj treść funkcji activeTaskDetails_DisplayedInUi() w runBlockingTest.
  2. Zanim uruchomisz fragment, zapisz activeTask w repozytorium.
repository.saveTask(activeTask)

Ostatni test wygląda tak:

TaskDetailsFragmentTest.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. Dodaj adnotacje do całej klasy za pomocą atrybutu @ExperimentalCoroutinesApi.

Po zakończeniu kod będzie wyglądał tak.

TaskDetailsFragmentTest.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. Uruchom test activeTaskDetails_DisplayedInUi().

Tak jak wcześniej, ten fragment powinien być widoczny, ale tym razem, bo poprawnie skonfigurowane repozytorium zawiera teraz informacje o zadaniu.


W tym kroku wykonasz pierwszy test integracji z biblioteki do testowania interfejsu Espresso. Udało Ci się utworzyć kod, dzięki czemu możesz dodawać testy z potwierdzeniami interfejsu. Możesz to zrobić w bibliotece espresso.

Espresso pomaga:

  • Korzystaj z widoków, takich jak klikanie przycisków, przesuwanie paska czy przewijanie ekranu w dół.
  • Twierdź, że określone widoki znajdują się na ekranie lub mają określony stan (na przykład zawierają określony tekst, zaznaczone pole wyboru itp.).

Krok 1. Zależność Gradle dla notatek

Będziesz już mieć główną zależność Espresso, ponieważ jest ona domyślnie włączona w projektach Android.

app/build.gradle

dependencies {

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

androidx.test.espresso:espresso-core – ta podstawowa zależność Espresso jest domyślnie uwzględniana podczas tworzenia nowego projektu na Androida. Zawiera podstawowy kod testowy dotyczący większości wyświetleń i działań na tych elementach.

Krok 2. Wyłącz animacje

Testy espresso odbywają się na prawdziwych urządzeniach, więc testy z użyciem instrumentacji są z natury rzetelne. Występuje problem z animacjami: jeśli animacja się opóźnia i próbujesz sprawdzić, czy wyświetlenie jest widoczne na ekranie, ale animacja wciąż jest animowana, Espresso może przypadkowo wykonać test. Może to powodować niestabilność testów espresso.

W przypadku testów interfejsu użytkownika Espresso najlepiej jest wyłączyć animacje (a teraz test będzie działać szybciej):

  1. Na urządzeniu testowym otwórz Ustawienia > Opcje programisty.
  2. Wyłącz te 3 ustawienia: Skala animacji okna, Skala animacji przejścia i Skala czasu trwania animacji.

Krok 3. Przyjrzyj się testowi espresso

Zanim napiszesz test z espresso, zerknij na kod z espresso.

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

Znajdują się tu pola wyboru o identyfikatorze task_detail_complete_checkbox, klikamy je i potwierdzamy, że jest ono zaznaczone.

Większość deklaracji dotyczących espresso składa się z 4 części:

1. Statyczna metoda espresso

onView

onView to przykład statycznej metody określającej działanie espresso. onView jest jedną z najpopularniejszych, ale dostępne są też inne opcje, na przykład onData.

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId to przykład elementu ViewMatcher, który uzyskuje widok danych według identyfikatora. Istnieją też inne odpowiedniki, które możesz sprawdzić w dokumentacji.

3. ViewAction

perform(click())

Metoda perform, która stosuje ViewAction. Element ViewAction można umieścić w widoku danych, np. klikając go.

4. ViewAssertion

check(matches(isChecked()))

check, która zajmuje ViewAssertion. Użytkownik ViewAssertion sprawdza lub sugeruje coś na temat tego widoku. Najpopularniejszym rodzajem hasła ViewAssertion jest asercja matches. Aby zakończyć potwierdzanie, użyj innego atrybutu ViewMatcher, w tym przypadku isChecked.

Pamiętaj, że nie zawsze wywołujesz zarówno operator perform, jak i check w instrukcji espresso. Możesz korzystać z potwierdzeń, które zawierają potwierdzeń check, oraz ViewAction.

  1. Otwórz TaskDetailFragmentTest.kt.
  2. Zaktualizuj test activeTaskDetails_DisplayedInUi.

TaskDetailsFragmentTest.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())))
    }

W razie potrzeby możesz użyć tych instrukcji:

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. Wszystko po komentarzu // THEN używa espresso. Przeanalizuj strukturę testów i zastosowanie znaczników withId, aby zobaczyć, jak powinna wyglądać strona z informacjami.
  2. Uruchom test i sprawdź, czy się on zakończył.

Krok 4. Opcjonalnie: napisz własny test do espresso

Teraz napisz test samodzielnie.

  1. Utwórz nowy test o nazwie completedTaskDetails_DisplayedInUi i skopiuj ten szkielet.

TaskDetailsFragmentTest.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. Patrząc na poprzedni test, ukończ ten test.
  2. Uruchom i potwierdź testy.

Ukończone completedTaskDetails_DisplayedInUi powinno wyglądać tak.

TaskDetailsFragmentTest.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()))
    }

Z tego ostatniego kroku dowiesz się, jak przetestować komponent nawigacji, używając innego typu testu (tzw. makiety), oraz biblioteki testowej Mockito.

W tym ćwiczeniu z programowania udało Ci się przeprowadzić podwójną wersję testową zwaną fałszywą. Fałszywe materiały to jeden z wielu typów podwójnych testów. Którego testu należy użyć do testowania komponentu Nawigacja?

Zastanów się, jak wygląda nawigacja. Wyobraź sobie, że naciskasz jedno z zadań w TasksFragment, by przejść do ekranu szczegółów zadania.

Oto kod w aplikacji TasksFragment, który po naciśnięciu wyświetla ekran z informacjami o zadaniu.

TasksFragment.kt

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


Nawigacja następuje z wywołaniem metody navigate. Jeśli trzeba będzie dodać potwierdzenie, nie ma pewności, czy udało Ci się przejść do TaskDetailFragment. Nawigacja to skomplikowane działanie, które nie skutkuje wyraźnym wynikiem ani zmianą stanu poza inicjowaniem elementu TaskDetailFragment.

Możesz stwierdzić, że metoda navigate została wywołana z prawidłowym parametrem działania. Właśnie tak działa podwójny test przykładu – sprawdza, czy zostały wywołane określone metody.

Mockito to platforma do tworzenia podwójnych testów. Słowo „przykład” jest używane w interfejsie API i w nazwie, ale nie służy tylko do tworzenia makiet. Może też sprawiać, że szpachle i szprychy.

Wykorzystasz kod Mockito do stworzenia makiety NavigationController, która pokazuje, że metoda nawigacji została poprawnie wywołana.

Krok 1. Dodaj zależności Gradle

  1. Dodaj zależności Gradle.

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 – jest to zależność Mockito.
  • dexmaker-mockito – ta biblioteka jest wymagana do korzystania z narzędzia Mockito w projekcie na Androida. Mockito musi generować zajęcia w czasie działania. W Androidzie jest to określane za pomocą kodu bajtowego dex, dlatego biblioteka ta umożliwia Mockito generowanie obiektów podczas działania w Androidzie.
  • androidx.test.espresso:espresso-contrib Ta biblioteka składa się z treści opublikowanych przez użytkowników zewnętrznych (są to nazwa), które zawierają kod testowy w przypadku bardziej zaawansowanych widoków, takich jak DatePicker i RecyclerView. Są tam również testy ułatwień dostępu i zajęcia o nazwie CountingIdlingResource, które omawiamy później.

Krok 2. Tworzenie testuFragmentów zadań

  1. Otwórz aplikację TasksFragment.
  2. Kliknij prawym przyciskiem myszy nazwę klasy TasksFragment i wybierz Wygeneruj, a następnie Przetestuj. Utwórz test w zestawie źródłowym androidTest.
  3. Skopiuj ten kod do TasksFragmentTest.

TasksTestTest.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()
    }

}

Ten kod wygląda podobnie do wpisanego kodu TaskDetailFragmentTest. Ustawia i wyłącza FakeAndroidTestRepository. Dodaj test nawigacyjny, aby sprawdzić, że po kliknięciu zadania na liście zadań wyświetla się właściwy adres TaskDetailFragment.

  1. Dodaj test clickTask_navigateToDetailFragmentOne.

TasksTestTest.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. Użyj funkcji mock Mockto, aby stworzyć przykładową kreację.

TasksTestTest.kt

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

Aby docenić się w Mockito, niech klasa poczuje się wybredna.

Następnie musisz powiązać element NavController z fragmentem. onFragment umożliwia wywoływanie metod w samym fragmencie.

  1. Utwórz nowy sympatyczny fragmentNavController.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Dodaj kod, aby kliknąć element z etykietą RecyclerView zawierający tekst "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions jest częścią biblioteki espresso-contrib i umożliwia wykonywanie działań Espresso na stronie RecyclerView.

  1. Sprawdź, czy element navigate został wywołany z prawidłowym argumentem.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Metoda Mocko

Pełny test wygląda tak:

@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. Uruchom test.

Aby sprawdzić, czy możesz się poruszać, możesz:

  1. Użyj szablonu Mockito, aby stworzyć przykładową kreację NavController.
  2. Załącz przykład do NavController.
  3. Sprawdź, czy nawigacja została wywołana z prawidłowym działaniem i parametrami.

Krok 3. Opcjonalnie: wpisz clickAddTaskButton_NawigacjaToAddEditFragment

Aby sprawdzić, czy możesz samodzielnie napisać test nawigacji, wykonaj to zadanie.

  1. Napisz test clickAddTaskButton_navigateToAddEditFragment, który potwierdzi, że jeśli klikniesz + FAFA, przejdziesz do: AddEditTaskFragment.

Odpowiedź znajduje się poniżej.

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

Kliknij tutaj, aby zobaczyć różnice między uruchomionym kodem a ostatecznym kodem.

Aby pobrać kod ukończonych ćwiczeń z programowania, możesz użyć poniższego polecenia git:

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


Możesz też pobrać repozytorium jako plik ZIP, rozpakować go i otworzyć w Android Studio.

Pobierz aplikację Zip

Z tego modułu ćwiczeń dowiesz się, jak skonfigurować ręczne wstrzykiwanie zależności, lokalizator usług oraz jak używać fałszywych przykładów w aplikacjach na Androida Kotlin. W szczególności:

  • To, co chcesz przetestować, i strategia testowania określają, jakiego typu testy zamierzasz wykonać. Testy jednostkowe są konkretne i szybkie. Testy integracji pozwalają sprawdzić interakcję między poszczególnymi częściami programu. Pełne testy umożliwiają weryfikację funkcji, mają najwyższą wierność i często są często instrumentowane, a ich działanie może potrwać dłużej.
  • Architektura aplikacji wpływa na to, jak trudne jest testowanie.
  • TDD lub Test Drive Development to strategia, w ramach której najpierw tworzysz testy, a potem tworzysz te funkcje, aby je zdać.
  • Aby odizolować części aplikacji na potrzeby testów, możesz używać podwójnych testów. Podwójna wersja testowa to wersja klasy przeznaczona do testowania. Na przykład fałszywe informacje pochodzą z bazy danych lub internetu.
  • Za pomocą wstrzyknięcia zależności zastąp prawdziwą klasę klasą testową, na przykład repozytorium lub warstwą sieciową.
  • Do uruchamiania komponentów interfejsu używaj testowania zaawansowanego (androidTest).
  • Jeśli nie możesz wstrzyknąć zależności zależności konstruktora, np. do uruchomienia fragmentu, często możesz użyć lokalizatora usług. Wzorzec lokalizatora usług jest alternatywą dla wstrzykiwania zależności. Polega ona na utworzeniu klasy pojedynczego typu o nazwie "Lokalizator usług&, który ma na celu podawanie zależności zarówno w standardowym, jak i w kodzie testowym.

Kurs Udacity:

Dokumentacja dla programistów Androida:

Materiały wideo:

Inne:

Linki do innych ćwiczeń z programowania znajdziesz w kursie dotyczącym programowania na Androida dla zaawansowanych w Kotlin.