Te warsztaty są częścią kursu Zaawansowany Android w Kotlinie. Najwięcej korzyści z tego kursu uzyskasz, jeśli przejdziesz wszystkie ćwiczenia w kolejności, ale nie jest to obowiązkowe. Wszystkie ćwiczenia z tego kursu znajdziesz na stronie docelowej ćwiczeń z zaawansowanego Androida w Kotlinie.
Wprowadzenie
Ten drugi moduł z serii poświęcony testowaniu dotyczy obiektów zastępczych: kiedy ich używać w Androidzie i jak je wdrażać za pomocą wstrzykiwania zależności, wzorca Service Locator i bibliotek. Dzięki temu dowiesz się, jak pisać:
- Testy jednostkowe repozytorium
- Testy integracji fragmentów i modelu widoku
- Testy nawigacji po fragmentach
Co warto wiedzieć
Musisz znać:
- Język programowania Kotlin
- Testowanie koncepcji omówionych w pierwszym laboratorium: pisanie i uruchamianie testów jednostkowych na Androidzie, używanie JUnit, Hamcrest, AndroidX Test, Robolectric oraz testowanie LiveData
- Te podstawowe biblioteki Androida Jetpack:
ViewModel
,LiveData
i komponent nawigacji. - Architektura aplikacji zgodna ze wzorcem z przewodnika po architekturze aplikacji i kursów podstawowych dotyczących Androida
- Podstawowe informacje o korutynach na Androidzie
Czego się nauczysz
- Jak zaplanować strategię testowania
- Jak tworzyć i używać obiektów zastępczych, czyli atrap i mocków
- Jak używać ręcznego wstrzykiwania zależności na Androidzie do testów jednostkowych i integracyjnych
- Jak stosować wzorzec lokalizatora usług
- Testowanie repozytoriów, fragmentów, modeli widoku i komponentu Navigation
Będziesz korzystać z tych bibliotek i koncepcji kodu:
Jakie zadania wykonasz
- Napisz testy jednostkowe dla repozytorium, używając testu zastępczego i wstrzykiwania zależności.
- Napisz testy jednostkowe dla modelu widoku za pomocą testu podwójnego i wstrzykiwania zależności.
- Pisać testy integracji fragmentów i ich modeli widoku za pomocą platformy testowej interfejsu Espresso.
- Pisać testy nawigacji za pomocą Mockito i Espresso.
W tej serii ćwiczeń będziesz pracować z aplikacją TO-DO Notes. Umożliwia ona zapisywanie zadań do wykonania i wyświetlanie ich na liście. Możesz je oznaczać jako ukończone lub nieukończone, filtrować lub usuwać.
Ta aplikacja jest napisana w języku Kotlin, ma kilka ekranów, korzysta z komponentów Jetpack i jest zgodna z architekturą opisaną w przewodniku po architekturze aplikacji. Dzięki temu, że dowiesz się, jak testować tę aplikację, będziesz w stanie testować aplikacje, które korzystają z tych samych bibliotek i architektury.
Pobieranie kodu
Aby rozpocząć, pobierz kod:
Możesz też sklonować repozytorium GitHub, aby uzyskać kod:
$ 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 poniższe instrukcje.
Krok 1. Uruchom przykładową aplikację
Po pobraniu aplikacji TO-DO otwórz ją w Android Studio i uruchom. Powinien się skompilować. Poznaj aplikację, wykonując te czynności:
- Utwórz nowe zadanie za pomocą pływającego przycisku czynności. Najpierw wpisz tytuł, a potem dodatkowe informacje o zadaniu. Zapisz go za pomocą pływającego przycisku czynności z zielonym znacznikiem wyboru.
- Na liście zadań kliknij tytuł zadania, które właśnie zostało ukończone, i sprawdź ekran szczegółów, aby zobaczyć resztę opisu.
- Na liście lub na ekranie szczegółów zaznacz pole wyboru tego zadania, aby ustawić jego stan na Ukończone.
- Wróć do ekranu zadań, otwórz menu filtra i filtruj zadania według stanu Aktywne i Ukończone.
- Otwórz panel nawigacji i kliknij Statystyki.
- Wróć do ekranu przeglądu i w menu panelu nawigacyjnego wybierz Wyczyść ukończone, aby usunąć wszystkie zadania ze stanem Ukończone.
Krok 2. Zapoznaj się z kodem przykładowej aplikacji
Aplikacja TO-DO jest oparta na popularnym przykładzie testowym i architektonicznym Architecture Blueprints (w wersji architektury reaktywnej). Aplikacja jest zgodna z architekturą opisaną w przewodniku po architekturze aplikacji. Korzysta z obiektów ViewModel z fragmentami, repozytorium i biblioteką Room. Jeśli znasz któryś z poniższych przykładów, ta aplikacja ma podobną architekturę:
- Ćwiczenia z programowania Room with a View
- Ćwiczenia z podstaw języka Kotlin na Androidzie
- Zaawansowane szkolenia z programowania aplikacji na Androida
- Przykładowa aplikacja Sunflower na Androida
- Kurs Developing Android Apps with Kotlin w Udacity
Ważniejsze jest zrozumienie ogólnej architektury aplikacji niż dogłębne poznanie logiki na dowolnej warstwie.
Oto podsumowanie pakietów, które znajdziesz:
Pakiet: | |
| Ekran dodawania lub edytowania zadania: kod warstwy interfejsu użytkownika do dodawania lub edytowania zadania. |
| Warstwa danych: dotyczy warstwy danych zadań. Zawiera kod bazy danych, sieci i repozytorium. |
| Ekran statystyk: kod warstwy interfejsu ekranu statystyk. |
| Ekran szczegółów zadania: kod warstwy interfejsu pojedynczego zadania. |
| Ekran zadań: kod warstwy interfejsu użytkownika dla listy wszystkich zadań. |
| Klasy narzędziowe: klasy udostępnione używane w różnych częściach aplikacji, np. w układzie odświeżania przez przesunięcie, który jest używany na wielu ekranach. |
Warstwa danych (.data)
Ta aplikacja zawiera symulowaną warstwę sieciową w pakiecie remote i warstwę bazy danych w pakiecie local. W tym projekcie warstwa sieci jest symulowana za pomocą funkcji HashMap
z opóźnieniem, a nie za pomocą rzeczywistych żądań sieciowych.
DefaultTasksRepository
koordynuje lub pośredniczy między warstwą sieciową a warstwą bazy danych i zwraca dane do warstwy interfejsu.
Warstwa interfejsu ( .addedittask, .statistics, .taskdetail, .tasks)
Każdy z pakietów warstwy interfejsu zawiera fragment i model widoku oraz inne klasy wymagane w interfejsie (np. adapter listy zadań). TaskActivity
to aktywność, która zawiera wszystkie fragmenty.
Nawigacja
Nawigacja w aplikacji jest kontrolowana przez komponent nawigacji. Jest on zdefiniowany w pliku nav_graph.xml
. Nawigacja jest wywoływana w modelach widoku za pomocą klasy Event
. Modele widoku określają też, jakie argumenty mają być przekazywane. Fragmenty obserwują Event
i wykonują rzeczywistą nawigację między ekranami.
Z tego ćwiczenia w Codelabs dowiesz się, jak testować repozytoria, modele widoków i fragmenty za pomocą obiektów testowych i wstrzykiwania zależności. Zanim dowiesz się, jakie to są testy, warto zrozumieć, jakie przesłanki będą Cię kierować przy ich pisaniu.
W tej sekcji omówimy kilka sprawdzonych metod testowania w ogóle, które mają zastosowanie w przypadku Androida.
Piramida testów
Gdy myślisz o strategii testowania, musisz wziąć pod uwagę 3 powiązane ze sobą aspekty:
- Zakres – jak dużą część kodu obejmuje test? Testy mogą być przeprowadzane na pojedynczej metodzie, w całej aplikacji lub w jej części.
- Szybkość – jak szybko przebiega test? Testowanie może trwać od milisekund do kilku minut.
- Wierność – jak bardzo test odzwierciedla rzeczywistość? Jeśli np. część testowanego kodu musi wysłać żądanie sieciowe, czy kod testowy faktycznie wysyła to żądanie, czy tylko symuluje wynik? Jeśli test faktycznie komunikuje się z siecią, oznacza to, że jest bardziej wiarygodny. W zamian test może trwać dłużej, powodować błędy w przypadku awarii sieci lub być kosztowny w użyciu.
Między tymi aspektami istnieją nieodłączne kompromisy. Na przykład szybkość i dokładność są ze sobą powiązane – im szybszy test, tym mniejsza dokładność i odwrotnie. Automatyczne testy można podzielić na 3 kategorie:
- Testy jednostkowe – są to bardzo szczegółowe testy, które są przeprowadzane na jednej klasie, zwykle na jednej metodzie w tej klasie. Jeśli test jednostkowy się nie powiedzie, możesz dokładnie określić, w którym miejscu kodu występuje problem. Testy te mają niską wierność, ponieważ w rzeczywistości aplikacja obejmuje znacznie więcej niż wykonanie jednej metody lub klasy. Działają one wystarczająco szybko, aby można je było uruchamiać za każdym razem, gdy zmieniasz kod. Najczęściej będą to testy przeprowadzane lokalnie (w
test
zbiorze źródeł). Przykład: testowanie pojedynczych metod w modelach widoku i repozytoriach. - Testy integracyjne – testują interakcję kilku klas, aby sprawdzić, czy działają zgodnie z oczekiwaniami, gdy są używane razem. Jednym ze sposobów strukturyzacji testów integracyjnych jest testowanie pojedynczej funkcji, np. możliwości zapisania zadania. Testują one większy zakres kodu niż testy jednostkowe, ale są zoptymalizowane pod kątem szybkości działania, a nie pełnej wierności. W zależności od sytuacji można je uruchamiać lokalnie lub jako testy z instrumentacją. Przykład: testowanie wszystkich funkcji pojedynczej pary fragmentu i modelu widoku.
- Testy kompleksowe – testowanie kombinacji funkcji działających razem. Testują one duże części aplikacji, dokładnie symulują rzeczywiste użycie, a dlatego zwykle działają wolno. Mają one największą dokładność i informują, czy aplikacja działa jako całość. Zasadniczo będą to testy z instrumentacją (w
androidTest
źródle)
Przykład: uruchomienie całej aplikacji i przetestowanie kilku funkcji jednocześnie.
Sugerowany odsetek tych testów jest często przedstawiany w postaci piramidy, w której zdecydowana większość to testy jednostkowe.
Architektura i testowanie
Możliwość testowania aplikacji na różnych poziomach piramidy testów jest nieodłącznie związana z architekturą aplikacji. Na przykład bardzo źle zaprojektowana aplikacja może umieszczać całą logikę w jednej metodzie. Możesz napisać testy kompleksowe, ponieważ zwykle sprawdzają one duże części aplikacji. Ale co z testami jednostkowymi lub integracyjnymi? Gdy cały kod znajduje się w jednym miejscu, trudno jest przetestować tylko kod związany z jedną jednostką lub funkcją.
Lepszym rozwiązaniem byłoby podzielenie logiki aplikacji na wiele metod i klas, co umożliwiłoby testowanie każdego elementu osobno. Architektura to sposób podziału i organizacji kodu, który ułatwia testy jednostkowe i integracyjne. Aplikacja TO-DO, którą będziesz testować, ma określoną architekturę:
W tym szkoleniu dowiesz się, jak testować poszczególne części powyższej architektury w odpowiedniej izolacji:
- Najpierw przetestujesz jednostkowo repozytorium.
- Następnie w modelu widoku użyjesz testowego zamiennika, który jest niezbędny do testowania jednostkowego i testowania integracyjnego modelu widoku.
- Następnie dowiesz się, jak pisać testy integracyjne fragmentów i ich modeli widoku.
- Na koniec dowiesz się, jak pisać testy integracyjne, które obejmują komponent Navigation.
Testy kompleksowe omówimy w następnej lekcji.
Gdy piszesz test jednostkowy dla części klasy (metody lub małej kolekcji metod), Twoim celem jest testowanie tylko kodu w tej klasie.
Testowanie kodu tylko w określonej klasie lub klasach może być trudne. Przeanalizujmy poniższy przykład. Otwórz data.source.DefaultTaskRepository
klasę w main
zestawie źródłowym. Jest to repozytorium aplikacji i klasa, dla której w następnym kroku napiszesz testy jednostkowe.
Twoim celem jest przetestowanie tylko kodu w tej klasie. Jednak DefaultTaskRepository
zależy od innych klas, takich jak LocalTaskDataSource
i RemoteTaskDataSource
. Inaczej mówiąc, LocalTaskDataSource
i RemoteTaskDataSource
są zależnościami interfejsu DefaultTaskRepository
.
Dlatego każda metoda w DefaultTaskRepository
wywołuje metody w klasach źródeł danych, które z kolei wywołują metody w innych klasach, aby zapisywać informacje w bazie danych lub komunikować się z siecią.
Na przykład zapoznaj się z tą metodą w 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 najbardziej „podstawowych” wywołań, jakie możesz wykonać w repozytorium. Ta metoda obejmuje odczytywanie z bazy danych SQLite i wykonywanie wywołań sieciowych (wywołanie updateTasksFromRemoteDataSource
). Wymaga to znacznie więcej kodu niż tylko kod repozytorium.
Oto bardziej szczegółowe powody, dla których testowanie repozytorium jest trudne:
- Aby przeprowadzić nawet najprostsze testy w tym repozytorium, musisz się zastanowić nad utworzeniem bazy danych i zarządzaniem nią. Pojawiają się wtedy pytania, np. „czy powinien to być test lokalny czy test z instrumentacją?” oraz „czy do uzyskania symulowanego środowiska Androida należy użyć AndroidX Test?”.
- Niektóre części kodu, np. kod sieciowy, mogą działać długo, a czasami nawet ulec awarii, co powoduje długotrwałe i niestabilne testy.
- Testy mogą utracić możliwość diagnozowania, który kod jest przyczyną niepowodzenia testu. Testy mogą zacząć testować kod spoza repozytorium, więc na przykład testy jednostkowe „repozytorium” mogą się nie powieść z powodu problemu w kodzie zależnym, np. w kodzie bazy danych.
Obiekty testowe
Rozwiązaniem tego problemu jest to, że podczas testowania repozytorium nie należy używać prawdziwego kodu sieciowego ani kodu bazy danych, ale zamiast tego używać testowego obiektu zastępczego. Obiekt testowy to wersja klasy utworzona specjalnie na potrzeby testowania. Ma ona zastępować prawdziwą wersję klasy w testach. To podobnie jak w przypadku dublera, który jest aktorem specjalizującym się w wykonywaniu wyczynów kaskaderskich i zastępuje prawdziwego aktora w niebezpiecznych scenach.
Oto niektóre rodzaje obiektów testowych:
Fałszywe | Jest to obiekt testowy, który ma „działającą” implementację klasy, ale jest ona zaimplementowana w taki sposób, że nadaje się do testów, ale nie do środowiska produkcyjnego. |
Mock | Obiekt testowy, który śledzi, które z jego metod zostały wywołane. Następnie przechodzi test lub nie, w zależności od tego, czy jego metody zostały wywołane prawidłowo. |
Stub | Obiekt testowy, który nie zawiera logiki i zwraca tylko to, co zaprogramujesz. |
Dummy | Obiekt testowy, który jest przekazywany, ale nieużywany, np. gdy musisz go tylko podać jako parametr. Jeśli masz |
Spy | Obiekt testowy, który śledzi też dodatkowe informacje, np. jeśli utworzysz |
Więcej informacji o obiektach testowych znajdziesz w artykule Testing on the Toilet: Know Your Test Doubles (po angielsku).
Najczęściej używane w Androidzie obiekty testowe to Fakes i Mocks.
W tym zadaniu utworzysz FakeDataSource
obiekt testowy, aby przeprowadzić test jednostkowy DefaultTasksRepository
niezależnie od rzeczywistych źródeł danych.
Krok 1. Utwórz klasę FakeDataSource
W tym kroku utworzysz klasę o nazwie FakeDataSouce
, która będzie podwójnym testem klasy LocalDataSource
i RemoteDataSource
.
- W zestawie źródeł test kliknij prawym przyciskiem myszy Nowy –> Pakiet.
- Utwórz pakiet data z pakietem source wewnątrz.
- Utwórz nową klasę o nazwie
FakeDataSource
w pakiecie data/source.
Krok 2. Zaimplementuj interfejs TasksDataSource
Aby można było użyć nowej klasy FakeDataSource
jako obiektu testowego, musi ona być w stanie zastąpić inne źródła danych. Źródła danych to TasksLocalDataSource
i TasksRemoteDataSource
.
- Zwróć uwagę, że oba te elementy implementują interfejs
TasksDataSource
.
class TasksLocalDataSource internal constructor(
private val tasksDao: TasksDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }
object TasksRemoteDataSource : TasksDataSource { ... }
- Spraw, aby
FakeDataSource
implementowałTasksDataSource
:
class FakeDataSource : TasksDataSource {
}
Android Studio zgłosi, że nie zaimplementowano wymaganych metod dla TasksDataSource
.
- Skorzystaj z menu szybkiej poprawki i wybierz Wdróż funkcję wspierania.
- Wybierz wszystkie metody i kliknij OK.
Krok 3. Zaimplementuj metodę getTasks w klasie FakeDataSource
FakeDataSource
to szczególny rodzaj obiektu testowego, zwany obiektem zastępczym. Obiekt zastępczy to obiekt testowy, który ma „działającą” implementację klasy, ale jest zaimplementowany w taki sposób, że nadaje się do testów, ale nie do środowiska produkcyjnego. „Działająca” implementacja oznacza, że klasa będzie generować realistyczne dane wyjściowe na podstawie danych wejściowych.
Na przykład fałszywe źródło danych nie będzie łączyć się z siecią ani zapisywać niczego w bazie danych – zamiast tego będzie używać listy w pamięci. Będzie to „działać zgodnie z oczekiwaniami”, ponieważ metody pobierania i zapisywania zadań będą zwracać oczekiwane wyniki, ale nigdy nie będzie można użyć tej implementacji w środowisku produkcyjnym, ponieważ nie jest ona zapisywana na serwerze ani w bazie danych.
FakeDataSource
- umożliwia testowanie kodu w
DefaultTasksRepository
bez konieczności korzystania z prawdziwej bazy danych lub sieci. - zapewnia „wystarczająco realistyczną” implementację na potrzeby testów.
- Zmień konstruktor
FakeDataSource
, aby utworzyćvar
o nazwietasks
, który jestMutableList<Task>?
z domyślną wartością pustej listy modyfikowalnej.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }
To lista zadań, które „udają” bazę danych lub odpowiedź serwera. Na razie celem jest przetestowanie metody getTasks
repozytorium. Wywołuje to metody źródła danych getTasks
, deleteAllTasks
i saveTask
.
Napisz fałszywą wersję tych metod:
- Zapisz
getTasks
: jeślitasks
nie jest równenull
, zwróć wynikSuccess
. Jeślitasks
ma wartośćnull
, zwróć wynikError
. - Zapisz
deleteAllTasks
: wyczyść listę zadań, które można zmieniać. - Napisz
saveTask
: dodaj zadanie do listy.
Te metody zaimplementowane dla FakeDataSource
wyglądają jak poniższy kod.
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 użyć tych instrukcji 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
Działa to podobnie jak rzeczywiste lokalne i zdalne źródła danych.
W tym kroku użyjesz techniki ręcznego wstrzykiwania zależności, aby móc używać utworzonego właśnie fałszywego obiektu testowego.
Główny problem polega na tym, że masz FakeDataSource
, ale nie wiadomo, jak używasz go w testach. Musi zastąpić atrybuty TasksRemoteDataSource
i TasksLocalDataSource
, ale tylko w testach. Zarówno TasksRemoteDataSource
, jak i TasksLocalDataSource
są zależnościami DefaultTasksRepository
, co oznacza, że DefaultTasksRepositories
wymaga tych klas do działania.
Obecnie zależności są tworzone w metodzie init
klasy 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 taskLocalDataSource
i tasksRemoteDataSource
w DefaultTasksRepository
, są one w zasadzie zakodowane na stałe. Nie ma możliwości zastąpienia testu podwójnego.
Zamiast tego przekaż te źródła danych do klasy, zamiast je na stałe w niej zakodować. Dostarczanie zależności jest znane jako wstrzykiwanie zależności. Zależności można podawać na różne sposoby, dlatego istnieją różne typy wstrzykiwania zależności.
Wstrzykiwanie zależności w konstruktorze umożliwia zastąpienie obiektu testowego przez przekazanie go do konstruktora.
Brak wstrzykiwania | Wstrzykiwanie |
Krok 1. Użyj wstrzykiwania zależności w konstruktorze w klasie DefaultTasksRepository
- Zmień konstruktor
DefaultTaskRepository
, aby zamiastApplication
przyjmował źródła danych i dyspozytor korutyny (który musisz też zamienić w testach – więcej informacji znajdziesz w trzeciej lekcji o korutynach).
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 }
- Ponieważ zależności zostały przekazane, usuń metodę
init
. Nie musisz już tworzyć zależności. - 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
- Na koniec zaktualizuj metodę
getRepository
, aby 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
}
}
}
}
Korzystasz teraz z wstrzykiwania zależności przez konstruktor.
Krok 2. Użyj FakeDataSource w testach
Teraz, gdy Twój kod korzysta z wstrzykiwania zależności w konstruktorze, możesz użyć fałszywego źródła danych do przetestowania funkcji DefaultTasksRepository
.
- Kliknij prawym przyciskiem myszy nazwę klasy
DefaultTasksRepository
i wybierz Generate (Wygeneruj), a następnie Test (Testuj). - Postępuj zgodnie z instrukcjami, aby utworzyć
DefaultTasksRepositoryTest
w testowym zestawie źródeł. - U góry nowej klasy
DefaultTasksRepositoryTest
dodaj zmienne członkowskie 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 }
- Utwórz 3 zmienne: 2 zmienne
FakeDataSource
(po jednej dla każdego źródła danych w repozytorium) i zmienną dlaDefaultTasksRepository
, którą będziesz testować.
DefaultTasksRepositoryTest.kt
private lateinit var tasksRemoteDataSource: FakeDataSource
private lateinit var tasksLocalDataSource: FakeDataSource
// Class under test
private lateinit var tasksRepository: DefaultTasksRepository
Utwórz metodę konfigurowania i inicjowania testowalnego DefaultTasksRepository
. Ten DefaultTasksRepository
będzie używać testowego dublera FakeDataSource
.
- Utwórz metodę o nazwie
createRepository
i dodaj do niej adnotację@Before
. - Utwórz instancje fałszywych źródeł danych, korzystając z list
remoteTasks
ilocalTasks
. - Utwórz instancję
tasksRepository
, używając 2 utworzonych przez siebie fałszywych źródeł danych iDispatchers.Unconfined
.
Ostateczna metoda powinna wyglądać tak, jak pokazano poniżej.
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. Napisz test funkcji DefaultTasksRepository getTasks()
Czas napisać test DefaultTasksRepository
.
- Napisz test dla metody
getTasks
w repozytorium. Sprawdź, czy po wywołaniu funkcjigetTasks
z parametremtrue
(co oznacza, że powinna ona ponownie załadować dane ze zdalnego źródła danych) zwraca dane ze zdalnego źródła danych (a nie z 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 wywoływania funkcji getTasks:
pojawi się błąd.
Krok 4. Dodaj runBlockingTest
Błąd korutyny jest oczekiwany, ponieważ getTasks
to funkcja suspend
i aby ją wywołać, musisz uruchomić korutynę. W tym celu potrzebujesz zakresu korutyny. Aby rozwiązać ten błąd, musisz dodać zależności Gradle do obsługi uruchamiania korutyn w testach.
- Dodaj wymagane zależności do testowania współprogramów do zestawu źródeł testowych za pomocą
testImplementation
.
app/build.gradle
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
Nie zapomnij o synchronizacji!
kotlinx-coroutines-test
to biblioteka testów coroutines, która jest przeznaczona specjalnie do testowania coroutines. Aby uruchomić testy, użyj funkcji runBlockingTest
. Jest to funkcja udostępniana przez bibliotekę testową coroutines. Przyjmuje blok kodu, a potem uruchamia go w specjalnym kontekście współprogramu, który działa synchronicznie i natychmiastowo, co oznacza, że działania będą wykonywane w określonej kolejności. Sprawia to, że Twoje korutyny działają jak zwykłe funkcje, więc jest to przydatne do testowania kodu.
Używaj runBlockingTest
w klasach testowych, gdy wywołujesz funkcję suspend
. Więcej informacji o tym, jak działa runBlockingTest
, i o testowaniu korutyn znajdziesz w następnym module z tej serii.
- Dodaj
@ExperimentalCoroutinesApi
nad klasą. Oznacza to, że w klasie używasz eksperymentalnego interfejsu API do tworzenia współprogramów (runBlockingTest
). W przeciwnym razie otrzymasz ostrzeżenie. - Wróć do
DefaultTasksRepositoryTest
i dodajrunBlockingTest
, aby cały test był traktowany jako „blok” kodu.
Ostatni test wygląda tak jak kod poniżej.
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))
}
}
- Uruchom nowy test
getTasks_requestsAllTasksFromRemoteDataSource
i sprawdź, czy działa, a błąd zniknął.
Właśnie pokazaliśmy, jak przeprowadzić test jednostkowy repozytorium. W kolejnych krokach ponownie użyjesz wstrzykiwania zależności i utworzysz kolejny obiekt testowy. Tym razem pokażemy, jak pisać testy jednostkowe i integracyjne dla modeli widoków.
Testy jednostkowe powinny tylko testować klasę lub metodę, która Cię interesuje. Jest to tzw. testowanie w izolacji, w którym wyraźnie izolujesz „jednostkę” i testujesz tylko kod, który jest jej częścią.
Dlatego TasksViewModelTest
powinien testować tylko kod TasksViewModel
– nie powinien testować klas bazy danych, sieci ani repozytorium. Dlatego w przypadku modeli widoku, podobnie jak w przypadku repozytorium, utworzysz fałszywe repozytorium i zastosujesz wstrzykiwanie zależności, aby używać go w testach.
W tym zadaniu zastosujesz wstrzykiwanie zależności do modeli widoku.
Krok 1. Tworzenie interfejsu TasksRepository
Pierwszym krokiem do użycia wstrzykiwania zależności w konstruktorze jest utworzenie wspólnego interfejsu, który będzie współdzielony przez klasę testową i rzeczywistą.
Jak to wygląda w praktyce? Spójrz na TasksRemoteDataSource
, TasksLocalDataSource
i FakeDataSource
i zauważ, że wszystkie mają ten sam interfejs: TasksDataSource
. Dzięki temu w konstruktorze klasy DefaultTasksRepository
możesz określić, że przyjmuje ona obiekt TasksDataSource
.
DefaultTasksRepository.kt
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
Dzięki temu możemy zastąpić FakeDataSource
.
Następnie utwórz interfejs dla DefaultTasksRepository
, tak jak w przypadku źródeł danych. Musi zawierać wszystkie publiczne metody (publiczny interfejs API) klasy DefaultTasksRepository
.
- Otwórz
DefaultTasksRepository
i kliknij prawym przyciskiem myszy nazwę zajęć. Następnie wybierz Refactor -> Extract -> Interface (Refaktoryzacja –> Wyodrębnij –> Interfejs).
- Kliknij Wyodrębnij do osobnego pliku.
- W oknie Wyodrębnij interfejs zmień nazwę interfejsu na
TasksRepository
. - W sekcji Members to form interface (Członkowie do utworzenia interfejsu) zaznacz wszystkich członków z wyjątkiem 2 członków towarzyszących i metod prywatnych.
- Kliknij Refactor (Refaktoryzuj). Nowy interfejs
TasksRepository
powinien pojawić się w pakiecie data/source .
A DefaultTasksRepository
teraz implementuje TasksRepository
.
- Uruchom aplikację (nie testy), aby sprawdzić, czy wszystko działa prawidłowo.
Krok 2. Tworzenie obiektu FakeTestRepository
Teraz, gdy masz już interfejs, możesz utworzyć DefaultTaskRepository
test double.
- W zestawie źródeł test w folderze data/source utwórz plik Kotlin i klasę
FakeTestRepository.kt
oraz rozszerz interfejsTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Pojawi się komunikat, że musisz zaimplementować metody interfejsu.
- Najedź kursorem na błąd, aż pojawi się menu sugestii, a następnie kliknij i wybierz Implement members (Zaimplementuj członków).
- Wybierz wszystkie metody i kliknij OK.
Krok 3. Implementowanie metod FakeTestRepository
Masz teraz klasę FakeTestRepository
z metodami „not implemented”. Podobnie jak w przypadku FakeDataSource
, FakeTestRepository
będzie obsługiwany przez strukturę danych, zamiast zajmować się skomplikowanym pośrednictwem między lokalnymi i zdalnymi źródłami danych.
Pamiętaj, że Twój model FakeTestRepository
nie musi używać FakeDataSource
ani niczego podobnego. Wystarczy, że będzie zwracać realistyczne fałszywe dane wyjściowe na podstawie danych wejściowych. Do przechowywania listy zadań użyjesz LinkedHashMap
, a do zadań obserwowanych – MutableLiveData
.
- W
FakeTestRepository
dodaj zarówno zmiennąLinkedHashMap
reprezentującą bieżącą listę zadań, jak iMutableLiveData
dla obserwowalnych zadań.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()
private val observableTasks = MutableLiveData<Result<List<Task>>>()
// Rest of class
}
Zaimplementuj te metody:
getTasks
– ta metoda powinna pobraćtasksServiceData
i przekształcić go w listę za pomocątasksServiceData.values.toList()
, a następnie zwrócić ją jako wynikSuccess
.refreshTasks
– aktualizuje wartośćobservableTasks
na wartość zwracaną przezgetTasks()
.observeTasks
— Tworzy korutynę za pomocąrunBlocking
i uruchamiarefreshTasks
, a następnie zwracaobservableTasks
.
Poniżej znajdziesz kod tych 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 funkcji addTasks
Podczas testowania lepiej mieć w repozytorium już jakieś Tasks
. Możesz wywoływać funkcję saveTask
kilka razy, ale aby to ułatwić, dodaj metodę pomocniczą specjalnie do testów, która umożliwia dodawanie zadań.
- Dodaj metodę
addTasks
, która przyjmujevararg
zadań, dodaje każde z nich doHashMap
, a następnie odświeża zadania.
FakeTestRepository.kt
fun addTasks(vararg tasks: Task) {
for (task in tasks) {
tasksServiceData[task.id] = task
}
runBlocking { refreshTasks() }
}
W tym momencie masz już fałszywe repozytorium do testowania z kilkoma zaimplementowanymi kluczowymi metodami. Następnie użyj go w testach.
W tym zadaniu użyjesz fałszywej klasy w ViewModel
. Użyj wstrzykiwania zależności w konstruktorze, aby przekazać 2 źródła danych za pomocą wstrzykiwania zależności w konstruktorze, dodając zmienną TasksRepository
do konstruktora TasksViewModel
.
W przypadku modeli widoku ten proces wygląda nieco inaczej, ponieważ nie tworzysz ich bezpośrednio. Na przykład:
class TasksFragment : Fragment() {
private val viewModel by viewModels<TasksViewModel>()
// Rest of class...
}
Podobnie jak w powyższym kodzie używasz viewModel's
delegata właściwości, który tworzy model widoku. Aby zmienić sposób tworzenia modelu widoku, musisz dodać i użyć ViewModelProvider.Factory
. Jeśli nie znasz ViewModelProvider.Factory
, więcej informacji znajdziesz tutaj.
Krok 1. Tworzenie i używanie ViewModelFactory w TasksViewModel
Zacznij od zaktualizowania klas i testów związanych z ekranem Tasks
.
- Otwórz
TasksViewModel
. - Zmień konstruktor klasy
TasksViewModel
, aby przyjmował obiektTasksRepository
zamiast go tworzyć w klasie.
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
}
Konstruktor został zmieniony, więc do utworzenia obiektu TasksViewModel
musisz teraz użyć fabryki. Umieść klasę fabryczną w tym samym pliku co TasksViewModel
, ale możesz też umieścić ją w osobnym pliku.
- U dołu pliku
TasksViewModel
, poza klasą, dodajTasksViewModelFactory
, który przyjmuje zwykłyTasksRepository
.
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)
}
To standardowy sposób zmiany sposobu tworzenia ViewModel
. Teraz, gdy masz już fabrykę, używaj jej wszędzie tam, gdzie tworzysz model widoku.
- Zaktualizuj aplikację
TasksFragment
, aby korzystać z fabryki.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TasksViewModel>()
// WITH
private val viewModel by viewModels<TasksViewModel> {
TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Uruchom kod aplikacji i sprawdź, czy wszystko nadal działa.
Krok 2. Używanie FakeTestRepository w klasie TasksViewModelTest
Teraz zamiast prawdziwego repozytorium w testach modelu widoku możesz używać fałszywego repozytorium.
- Otwórz
TasksViewModelTest
. - Dodaj właściwość
FakeTestRepository
wTasksViewModelTest
.
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
}
- Zaktualizuj metodę
setupViewModel
, aby utworzyćFakeTestRepository
z 3 zadaniami, a następnie skonstruujtasksViewModel
za pomocą tego repozytorium.
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)
}
- Ponieważ nie używasz już kodu AndroidX Test
ApplicationProvider.getApplicationContext
, możesz też usunąć adnotację@RunWith(AndroidJUnit4::class)
. - Przeprowadź testy i sprawdź, czy wszystko działa.
Dzięki wstrzykiwaniu zależności w konstruktorze usunęliśmy DefaultTasksRepository
jako zależność i zastąpiliśmy go w testach elementem FakeTestRepository
.
Krok 3. Aktualizowanie też fragmentu TaskDetail i widoku modelu
Wprowadź dokładnie te same zmiany w przypadku atrybutów TaskDetailFragment
i TaskDetailViewModel
. Przygotuje to kod do napisania TaskDetail
testów.
- Otwórz
TaskDetailViewModel
. - Zaktualizuj 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 }
- U dołu pliku
TaskDetailViewModel
, poza klasą, dodajTaskDetailViewModelFactory
.
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)
}
- Zaktualizuj aplikację
TasksFragment
, aby korzystać z fabryki.
TasksFragment.kt
// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()
// WITH
private val viewModel by viewModels<TaskDetailViewModel> {
TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
- Uruchom kod i sprawdź, czy wszystko działa.
Teraz możesz używać FakeTestRepository
zamiast prawdziwego repozytorium w przypadku TasksFragment
i TasksDetailFragment
.
Następnie napiszesz testy integracyjne, aby sprawdzić interakcje fragmentu i modelu widoku. Sprawdzisz, czy kod modelu widoku prawidłowo aktualizuje interfejs. W tym celu użyj
- wzorzec ServiceLocator,
- biblioteki Espresso i Mockito,
Testy integracyjne sprawdzają interakcję kilku klas, aby upewnić się, że działają one zgodnie z oczekiwaniami, gdy są używane razem. Testy te można uruchamiać lokalnie (test
zestaw źródeł) lub jako testy instrumentacyjne (androidTest
zestaw źródeł).
W Twoim przypadku będziesz brać każdy fragment i pisać testy integracyjne dla fragmentu i modelu widoku, aby przetestować główne funkcje fragmentu.
Krok 1. Dodawanie zależności Gradle
- 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"
Zależności te obejmują:
junit:junit
– JUnit, który jest niezbędny do pisania podstawowych instrukcji testowych.androidx.test:core
– podstawowa biblioteka testowa AndroidXkotlinx-coroutines-test
– biblioteka do testowania współprogramów;androidx.fragment:fragment-testing
– biblioteka testowa AndroidX do tworzenia fragmentów w testach i zmiany ich stanu.
Ponieważ będziesz używać tych bibliotek w androidTest
zestawie źródeł, dodaj je jako zależności za pomocą androidTestImplementation
.
Krok 2. Utwórz klasę TaskDetailFragmentTest
TaskDetailFragment
– zawiera informacje o jednym zadaniu.
Zacznij od napisania testu fragmentu TaskDetailFragment
, ponieważ ma on dość podstawową funkcjonalność w porównaniu z innymi fragmentami.
- Otwórz
taskdetail.TaskDetailFragment
. - Wygeneruj test dla
TaskDetailFragment
, tak jak wcześniej. Zaakceptuj domyślne wybory i umieść je w zestawie źródeł androidTest (NIE w zestawie źródełtest
).
- Dodaj do klasy
TaskDetailFragmentTest
te adnotacje:
TaskDetailFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
Celem tych adnotacji jest:
@MediumTest
– oznacza test jako test integracji o „średnim czasie działania” (w przeciwieństwie do testów jednostkowych@SmallTest
i dużych testów kompleksowych@LargeTest
). Ułatwia to grupowanie i wybieranie rozmiaru testu do przeprowadzenia.@RunWith(AndroidJUnit4::class)
– używane w każdej klasie korzystającej z AndroidX Test.
Krok 3. Uruchamianie fragmentu z testu
W tym zadaniu uruchomisz TaskDetailFragment
za pomocą biblioteki AndroidX Testing. FragmentScenario
to klasa z AndroidX Test, która otacza fragment i zapewnia bezpośrednią kontrolę nad cyklem życia fragmentu na potrzeby testowania. Aby napisać testy fragmentów, utwórz FragmentScenario
dla testowanego fragmentu (TaskDetailFragment
).
- Skopiuj ten test do usługi
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)
}
Ten kod powyżej:
- Tworzy zadanie.
- Tworzy obiekt
Bundle
, który reprezentuje argumenty fragmentu zadania przekazywane do fragmentu. - Funkcja
launchFragmentInContainer
tworzyFragmentScenario
z tym pakietem i motywem.
To nie jest jeszcze ukończony test, ponieważ nie zawiera żadnych asercji. Na razie przeprowadź test i obserwuj, co się stanie.
- Jest to test z użyciem instrumentacji, więc upewnij się, że emulator lub urządzenie są widoczne.
- Uruchom test.
Powinno się wydarzyć kilka rzeczy.
- Po pierwsze, ponieważ jest to test z instrumentacją, zostanie on przeprowadzony na urządzeniu fizycznym (jeśli jest podłączone) lub na emulatorze.
- Powinien uruchomić fragment.
- Zwróć uwagę, że nie przechodzi on do żadnego innego fragmentu ani nie ma żadnych menu powiązanych z działaniem – jest to tylko fragment.
Na koniec przyjrzyj się uważnie i zauważ, że fragment zawiera komunikat „Brak danych”, ponieważ nie udało się wczytać danych zadania.
Test musi wczytać plik TaskDetailFragment
(co już zostało zrobione) i sprawdzić, czy dane zostały wczytane prawidłowo. 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 ten FakeTestRepository
, ale potrzebujesz sposobu na zastąpienie prawdziwego repozytorium fałszywym w przypadku fragmentu. Zrobisz to w następnym kroku.
W tym zadaniu udostępnisz fałszywe repozytorium fragmentowi za pomocą ServiceLocator
. Umożliwi Ci to pisanie testów integracyjnych fragmentu i modelu widoku.
Nie możesz tu używać wstrzykiwania zależności w konstruktorze, jak to było wcześniej, gdy trzeba było przekazać zależność do modelu widoku lub repozytorium. Wstrzykiwanie zależności przez konstruktor wymaga utworzenia klasy. Fragmenty i aktywności to przykłady klas, których nie tworzysz i do których konstruktora zwykle nie masz dostępu.
Fragmentu nie tworzysz samodzielnie, więc nie możesz użyć wstrzykiwania zależności w konstruktorze, aby zamienić w nim testowy obiekt zastępczy repozytorium (FakeTestRepository
). Zamiast tego użyj wzorca lokalizatora usług. Wzorzec lokalizatora usług jest alternatywą dla wstrzykiwania zależności. Polega ona na utworzeniu klasy singleton o nazwie „Service Locator”, której zadaniem jest dostarczanie zależności zarówno do zwykłego kodu, jak i do kodu testowego. W zwykłym kodzie aplikacji (zestaw źródeł main
) wszystkie te zależności są zwykłymi zależnościami aplikacji. W przypadku testów zmodyfikuj lokalizator usług, aby udostępniał wersje zastępcze zależności.
Nie korzystasz z lokalizatora usług | Korzystanie z lokalizatora usług |
W przypadku aplikacji z tego ćwiczenia wykonaj te czynności:
- Utwórz klasę lokalizatora usług, która może tworzyć i przechowywać repozytorium. Domyślnie tworzy „normalne” repozytorium.
- Przeprowadź refaktoryzację kodu, aby w razie potrzeby używać lokalizatora usług.
- W klasie testowej wywołaj metodę w lokalizatorze usług, która zastąpi „normalne” repozytorium podwójnym testem.
Krok 1. Tworzenie obiektu ServiceLocator
Utwórzmy zajęcia ServiceLocator
. Będzie on znajdować się w głównym zestawie źródeł wraz z pozostałą częścią kodu aplikacji, ponieważ jest używany przez główny kod aplikacji.
Uwaga: ServiceLocator
to singleton, więc w przypadku klasy użyj słowa kluczowego object
w Kotlinie.
- Utwórz plik ServiceLocator.kt w głównym zestawie źródeł.
- Zdefiniuj
object
o nazwieServiceLocator
. - Utwórz zmienne instancji
database
irepository
i ustaw dla nich wartośćnull
. - Dodaj do repozytorium adnotację
@Volatile
, ponieważ może być ono używane przez wiele wątków (szczegółowe wyjaśnienie symbolu@Volatile
znajdziesz tutaj).
Kod powinien wyglądać tak, jak pokazano poniżej.
object ServiceLocator {
private var database: ToDoDatabase? = null
@Volatile
var tasksRepository: TasksRepository? = null
}
Obecnie jedyne, co musi robić Twój ServiceLocator
, to zwracać TasksRepository
. W razie potrzeby zwróci istniejący obiekt DefaultTasksRepository
lub utworzy i zwróci nowy obiekt DefaultTasksRepository
.
Zdefiniuj te funkcje:
provideTasksRepository
– udostępnia istniejące repozytorium lub tworzy nowe. Aby uniknąć przypadkowego utworzenia dwóch instancji repozytorium w sytuacjach, w których działa wiele wątków, ta metoda powinna byćsynchronized
wthis
.createTasksRepository
– kod do tworzenia nowego repozytorium. WywołacreateTaskLocalDataSource
i utworzy nowyTasksRemoteDataSource
.createTaskLocalDataSource
– kod do tworzenia nowego lokalnego źródła danych. Zadzwonię pod numercreateDataBase
.createDataBase
– kod do tworzenia nowej bazy danych.
Gotowy kod znajdziesz 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 klasy ServiceLocator w aplikacji
Wprowadzisz zmianę w głównym kodzie aplikacji (nie w testach), aby utworzyć repozytorium w jednym miejscu, czyli w ServiceLocator
.
Ważne jest, aby utworzyć tylko jedną instancję klasy repozytorium. Aby to zapewnić, użyjesz lokalizatora usług w klasie Application.
- Na najwyższym poziomie hierarchii pakietów otwórz
TodoApplication
i utwórzval
dla repozytorium, a następnie przypisz mu repozytorium uzyskane 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())
}
}
Po utworzeniu repozytorium w aplikacji możesz usunąć starą metodę getRepository
w DefaultTasksRepository
.
- 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 wszędzie, gdzie używasz getRepository
, używaj taskRepository
aplikacji. Dzięki temu zamiast tworzyć repozytorium bezpośrednio, otrzymasz repozytorium udostępnione przez ServiceLocator
.
- Otwórz
TaskDetailFragement
i znajdź wywołaniegetRepository
u góry klasy. - Zastąp to wywołanie wywołaniem, które pobiera repozytorium z domeny
TodoApplication
.
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)
}
- To samo zrób 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)
}
- W przypadku
StatisticsViewModel
iAddEditTaskViewModel
zaktualizuj kod, który pobiera repozytorium, aby używać repozytorium zTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- Uruchom aplikację (nie test)!
Po refaktoryzacji aplikacja powinna działać bez problemów.
Krok 3. Tworzenie repozytorium FakeAndroidTestRepository
W zestawie źródeł testowych masz już FakeTestRepository
. Domyślnie nie możesz udostępniać klas testowych między zestawami źródeł test
i androidTest
. Musisz więc utworzyć zduplikowaną klasę FakeTestRepository
w zestawie źródeł androidTest
i nadać jej nazwę FakeAndroidTestRepository
.
- Kliknij prawym przyciskiem myszy zestaw źródłowy
androidTest
i utwórz pakiet danych. Ponownie kliknij prawym przyciskiem myszy i utwórz pakiet źródłowy . - Utwórz w tym pakiecie źródłowym nową klasę o nazwie
FakeAndroidTestRepository.kt
. - Skopiuj do tych zajęć poniższy kod.
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 klasy ServiceLocator do testów
Czas użyć ServiceLocator
, aby podczas testowania zamienić testowe obiekty zastępcze. Aby to zrobić, musisz dodać kod do kodu ServiceLocator
.
- Otwórz
ServiceLocator.kt
. - Oznacz setter dla
tasksRepository
jako@VisibleForTesting
. Ta adnotacja informuje, że powodem, dla którego setter jest publiczny, są testy.
ServiceLocator.kt
@Volatile
var tasksRepository: TasksRepository? = null
@VisibleForTesting set
Niezależnie od tego, czy test jest przeprowadzany samodzielnie, czy w grupie testów, powinien działać dokładnie tak samo. Oznacza to, że testy nie powinny mieć żadnych zachowań, które są od siebie zależne (co oznacza unikanie udostępniania obiektów między testami).
Ponieważ ServiceLocator
jest singletonem, może zostać przypadkowo udostępniony między testami. Aby tego uniknąć, utwórz metodę, która prawidłowo resetuje stan ServiceLocator
między testami.
- Dodaj zmienną instancji o nazwie
lock
z wartościąAny
.
ServiceLocator.kt
private val lock = Any()
- Dodaj metodę przeznaczoną do testowania o nazwie
resetRepository
, która czyści bazę danych i ustawia zarówno repozytorium, jak i 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. Używanie ServiceLocator
W tym kroku użyjesz ServiceLocator
.
- Otwórz
TaskDetailFragmentTest
. - Zadeklaruj zmienną
lateinit TasksRepository
. - Dodaj metodę konfiguracji i metodę zamykania, aby skonfigurować
FakeAndroidTestRepository
przed każdym testem i wyczyścić go po każdym teście.
TaskDetailFragmentTest.kt
private lateinit var repository: TasksRepository
@Before
fun initRepository() {
repository = FakeAndroidTestRepository()
ServiceLocator.tasksRepository = repository
}
@After
fun cleanupDb() = runBlockingTest {
ServiceLocator.resetRepository()
}
- Owiń treść funkcji
activeTaskDetails_DisplayedInUi()
wrunBlockingTest
. - Zanim uruchomisz fragment, zapisz
activeTask
w repozytorium.
repository.saveTask(activeTask)
Ostateczny test wygląda tak jak poniższy kod.
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)
}
- Dodaj adnotacje do całej klasy za pomocą
@ExperimentalCoroutinesApi
.
Po zakończeniu kod będzie wyglądać tak.
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)
}
}
- Uruchom test
activeTaskDetails_DisplayedInUi()
.
Podobnie jak wcześniej, powinien być widoczny fragment, ale tym razem, ponieważ repozytorium zostało prawidłowo skonfigurowane, wyświetlają się informacje o zadaniu.
W tym kroku użyjesz biblioteki testów interfejsu Espresso, aby przeprowadzić pierwszy test integracyjny. Kod jest uporządkowany w taki sposób, że możesz dodawać testy z asercjami interfejsu. W tym celu użyjesz biblioteki testowej Espresso.
Espresso pomaga:
- interakcji z widokami, np. klikania przycisków, przesuwania paska lub przewijania ekranu w dół;
- Sprawdzanie, czy określone widoki są widoczne na ekranie lub czy są w określonym stanie (np. czy zawierają określony tekst lub czy pole wyboru jest zaznaczone itp.).
Krok 1. Uwaga dotycząca zależności Gradle
Główna zależność Espresso jest już dostępna, ponieważ jest domyślnie uwzględniana w projektach na Androida.
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 dla większości widoków i działań w nich.
Krok 2. Wyłączanie animacji
Testy Espresso są przeprowadzane na rzeczywistym urządzeniu, więc z natury są testami z instrumentacją. Jednym z problemów są animacje: jeśli animacja się opóźnia i próbujesz sprawdzić, czy widok jest na ekranie, ale animacja nadal trwa, Espresso może przypadkowo zakończyć test niepowodzeniem. Może to powodować niestabilność testów Espresso.
W przypadku testów interfejsu Espresso zalecamy wyłączenie animacji (testy będą też działać szybciej):
- Na urządzeniu testowym otwórz Ustawienia > Opcje programisty.
- Wyłącz te 3 ustawienia: Skala animacji okna, Skala animacji przejścia i Skala długości animacji.
Krok 3. Przykład testu Espresso
Zanim napiszesz test Espresso, zapoznaj się z kodem Espresso.
onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))
To polecenie znajduje widok pola wyboru o identyfikatorze task_detail_complete_checkbox
, klika go, a następnie sprawdza, czy jest zaznaczone.
Większość instrukcji Espresso składa się z 4 części:
1. Metoda statycznego espresso
onView
onView
to przykład statycznej metody Espresso, która rozpoczyna instrukcję Espresso. onView
jest jednym z najpopularniejszych, ale są też inne opcje, np. onData
.
2. ViewMatcher
withId(R.id.task_detail_title_text)
withId
to przykład ViewMatcher
, który pobiera widok według identyfikatora. W dokumentacji znajdziesz inne narzędzia do dopasowywania widoków.
3. ViewAction
perform(click())
Metoda perform
, która przyjmuje argument ViewAction
. ViewAction
to czynność, którą można wykonać w widoku, np. kliknąć go.
check(matches(isChecked()))
check
, co zajmuje ViewAssertion
. ViewAssertion
s sprawdza lub potwierdza coś w widoku. Najczęściej używanym ViewAssertion
jest asercja matches
. Aby zakończyć asercję, użyj innego znaku ViewMatcher
, w tym przypadku isChecked
.
Pamiętaj, że w instrukcji Espresso nie zawsze wywołujesz oba elementy perform
i check
. Możesz mieć instrukcje, które tylko potwierdzają coś za pomocą check
lub tylko wykonują działanie ViewAction
za pomocą perform
.
- Otwórz
TaskDetailFragmentTest.kt
. - Zaktualizuj test
activeTaskDetails_DisplayedInUi
.
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())))
}
W razie potrzeby możesz użyć tych instrukcji importowania:
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
- Wszystko, co znajduje się po komentarzu
// THEN
, korzysta z Espresso. Sprawdź strukturę testu i użycie symboluwithId
oraz sprawdź, czy są w nim stwierdzenia dotyczące wyglądu strony szczegółów. - Uruchom test i sprawdź, czy został zaliczony.
Krok 4. Opcjonalnie: napisz własny test Espresso
Teraz napisz test samodzielnie.
- Utwórz nowy test o nazwie
completedTaskDetails_DisplayedInUi
i skopiuj ten kod szkieletowy.
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
}
- Na podstawie poprzedniego testu ukończ ten test.
- Uruchom test i potwierdź, że został zaliczony.
Gotowy completedTaskDetails_DisplayedInUi
powinien wyglądać jak ten kod.
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()))
}
W tym ostatnim kroku dowiesz się, jak przetestować komponent nawigacji za pomocą innego rodzaju obiektu testowego, zwanego atrapą, oraz biblioteki testowej Mockito.
W tym module wykorzystaliśmy testowy obiekt zastępczy zwany fałszywym obiektem. Obiekty zastępcze to jeden z wielu rodzajów testowych duplikatów. Jakiego obiektu testowego należy użyć do testowania komponentu Navigation?
Zastanów się, jak odbywa się nawigacja. Wyobraź sobie, że klikasz jedno z zadań w sekcji TasksFragment
, aby przejść do ekranu szczegółów zadania.
Oto kod w TasksFragment
, który po naciśnięciu przenosi użytkownika na ekran szczegółów zadania.
TasksFragment.kt
private fun openTaskDetails(taskId: String) {
val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
findNavController().navigate(action)
}
Nawigacja następuje z powodu wywołania metody navigate
. Jeśli musisz napisać instrukcję assert, nie ma prostego sposobu na sprawdzenie, czy nastąpiło przejście do TaskDetailFragment
. Nawigacja to złożona czynność, która nie daje jasnego wyniku ani zmiany stanu poza zainicjowaniem TaskDetailFragment
.
Możesz sprawdzić, czy metoda navigate
została wywołana z prawidłowym parametrem działania. Właśnie to robi obiekt testowy typu mock – sprawdza, czy wywołano określone metody.
Mockito to platforma do tworzenia obiektów testowych. Chociaż w interfejsie API i nazwie użyto słowa „mock”, nie służy on tylko do tworzenia wersji demonstracyjnych. Może też tworzyć atrapy i szpiegi.
Użyjesz Mockito, aby utworzyć obiekt zastępczy NavigationController
, który może potwierdzić, że metoda navigate została wywołana prawidłowo.
Krok 1. Dodawanie zależności Gradle
- 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
– to zależność Mockito.dexmaker-mockito
– ta biblioteka jest wymagana do używania Mockito w projekcie na Androida. Mockito musi generować klasy w czasie działania. Na Androidzie odbywa się to za pomocą kodu bajtowego dex, dlatego ta biblioteka umożliwia Mockito generowanie obiektów w czasie działania na Androidzie.androidx.test.espresso:espresso-contrib
– ta biblioteka składa się z zewnętrznych kontrybucji (stąd nazwa), które zawierają kod testowy dla bardziej zaawansowanych widoków, takich jakDatePicker
iRecyclerView
. Zawiera też testy ułatwień dostępu i klasę o nazwieCountingIdlingResource
, o której piszemy w dalszej części.
Krok 2. Create TasksFragmentTest
- Otwórz pokój
TasksFragment
. - Kliknij prawym przyciskiem myszy nazwę klasy
TasksFragment
i wybierz kolejno Wygeneruj i Testuj. Utwórz test w zestawie źródeł androidTest. - Skopiuj ten kod do
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()
}
}
Ten kod jest podobny do kodu TaskDetailFragmentTest
, który został przez Ciebie napisany. Konfiguruje i usuwa FakeAndroidTestRepository
. Dodaj test nawigacji, aby sprawdzić, czy po kliknięciu zadania na liście zadań następuje przejście do odpowiedniego TaskDetailFragment
.
- Dodaj test
clickTask_navigateToDetailFragmentOne
.
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)
}
- Aby utworzyć mock, użyj funkcji
mock
Mockito.
TasksFragmentTest.kt
val navController = mock(NavController::class.java)
Aby utworzyć obiekt pozorny w Mockito, przekaż klasę, którą chcesz zamodelować.
Następnie musisz powiązać urządzenie NavController
z fragmentem. onFragment
umożliwia wywoływanie metod w samym fragmencie.
- Ustaw nowy mock jako
NavController
fragmentu.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- Dodaj kod, aby kliknąć element w
RecyclerView
, który zawiera 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 widoku RecyclerView.
- Sprawdź, czy wywołano funkcję
navigate
z prawidłowym argumentem.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
Metoda verify
w Mockito sprawia, że jest to mock – możesz potwierdzić, że wywołano w nim konkretną metodę (navigate
) z parametrem (actionTasksFragmentToTaskDetailFragment
z identyfikatorem „id1”).navController
Kompletny 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")
)
}
- Przeprowadź test.
Podsumowując, aby przetestować nawigację, możesz:
- Użyj Mockito, aby utworzyć obiekt zastępczy
NavController
. - Dołącz do fragmentu zasób
NavController
. - Sprawdź, czy funkcja navigate została wywołana z prawidłowym działaniem i parametrami.
Krok 3. Opcjonalnie, write clickAddTaskButton_navigateToAddEditFragment
Aby sprawdzić, czy możesz samodzielnie przeprowadzić test nawigacji, wykonaj to zadanie.
- Napisz test
clickAddTaskButton_navigateToAddEditFragment
, który sprawdza, czy po kliknięciu przycisku FAB + następuje przejście do elementuAddEditTaskFragment
.
Odpowiedź znajdziesz poniżej.
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)
)
)
}
Kliknij tutaj, aby zobaczyć różnicę między kodem początkowym a końcowym.
Aby pobrać kod ukończonego ćwiczenia, możesz użyć tego 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.
W tym laboratorium dowiedzieliśmy się, jak skonfigurować ręczne wstrzykiwanie zależności i lokalizator usług oraz jak używać atrap i mocków w aplikacjach na Androida napisanych w Kotlinie. W szczególności:
- Rodzaj testów, które chcesz przeprowadzić w swojej aplikacji, zależy od tego, co chcesz przetestować, i od Twojej strategii testowania. Testy jednostkowe są ukierunkowane i szybkie. Testy integracji weryfikują interakcje między częściami programu. Testy kompleksowe weryfikują funkcje, mają najwyższą wierność, są często instrumentowane i mogą trwać dłużej.
- Architektura aplikacji wpływa na to, jak trudne jest jej testowanie.
- TDD, czyli programowanie sterowane testami, to strategia, w której najpierw piszesz testy, a potem tworzysz funkcję, która je przechodzi.
- Aby wyodrębnić części aplikacji do testowania, możesz użyć obiektów zastępczych. Obiekt testowy to wersja klasy utworzona specjalnie na potrzeby testowania. Możesz na przykład symulować pobieranie danych z bazy danych lub internetu.
- Użyj wstrzykiwania zależności, aby zastąpić prawdziwą klasę klasą testową, np. repozytorium lub warstwą sieciową.
- Użyj testów z instrumentacją (
androidTest
), aby uruchomić komponenty interfejsu. - Jeśli nie możesz użyć wstrzykiwania zależności w konstruktorze, np. aby uruchomić fragment, często możesz użyć lokalizatora usług. Wzorzec lokalizatora usług to alternatywa dla wstrzykiwania zależności. Polega ona na utworzeniu klasy singleton o nazwie „Service Locator”, której zadaniem jest dostarczanie zależności zarówno do zwykłego kodu, jak i do kodu testowego.
Kurs Udacity:
Dokumentacja dla deweloperów aplikacji na Androida:
- Przewodnik po architekturze aplikacji
runBlocking
irunBlockingTest
FragmentScenario
- Espresso
- Mockito
- JUnit4
- Biblioteka testowa AndroidX
- Biblioteka testowa AndroidX Architecture Components Core
- Zestawy źródeł
- Testowanie z wiersza poleceń
Materiały wideo:
Inne:
Linki do innych ćwiczeń z tego kursu znajdziesz na stronie docelowej ćwiczeń z zaawansowanego Androida w Kotlinie.