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:
- Język programowania Kotlin
- Testy pojęte podczas pierwszego ćwiczenia z programowania: pisanie i uruchamianie testów jednostkowych na urządzeniach z Androidem przy użyciu JUnit, Hamcrest, testu Androida X, Robolectric, a także testowania LiveData
- Te główne biblioteki Jetpack na Androida:
ViewModel
,LiveData
i komponent nawigacji - architektury aplikacji zgodnej ze wskazówkami dotyczącymi architektury aplikacji i ćwiczeń z programowania na Androida,
- Podstawowe zasady dotyczące urządzeń z Androidem
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:
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ę:
- Sala z widokiem z programowania
- Ćwiczenia z programowania – podstawy Kotlin na Androida
- Zaawansowane ćwiczenia programowania na Androidzie
- Przykład słonecznika na Androida
- Kurs dotyczący tworzenia aplikacji na Androida z pomocą kursu Kotlin Udacity
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: | |
| Dodawanie lub edytowanie ekranu zadania: kod warstwy interfejsu służący do dodawania i edytowania zadań. |
| Warstwa danych: dotyczy warstwy danych z zadaniami. Zawiera on bazę danych, sieć i kod repozytorium. |
| Ekran statystyk: kod warstwy interfejsu wyświetlany na ekranie statystyk. |
| Ekran szczegółów zadania: kod warstwy interfejsu dotyczący pojedynczego zadania. |
| Ekran Listy zadań: kod warstwy interfejsu zawierający listę wszystkich zadań. |
| 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ą:
- Najpierw przetestuj jednostkę w repozytorium.
- 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.
- Następnie nauczysz się pisać testy integracji dla fragmentów i ich modeli widoku danych.
- 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 |
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 |
Szpiegostwo | Podwójny test pozwala też śledzić dodatkowe informacje. Jeśli np. wywołałeś funkcję |
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
.
- W zestawie źródłowym testowym kliknij prawym przyciskiem myszy Nowy -> Pakiet.
- Utwórz pakiet danych z pakietem źródłowym.
- 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
.
- 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 { ... }
- Skonfiguruj
FakeDataSource
:TasksDataSource
class FakeDataSource : TasksDataSource {
}
Android Studio zgłosi, że nie masz zastosowanych wymaganych metod dla interfejsu TasksDataSource
.
- Użyj menu szybkiej naprawy i wybierz Wdróż członków.
- 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 ¶metru 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”.
- Zmień konstruktor
FakeDataSource
, aby utworzyćvar
o nazwietasks
o wartościMutableList<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:
- Napisz
getTasks
: jeślitasks
nie jestnull
, zwróć wynikSuccess
. Jeślitasks
ma wartośćnull
, zwróć wynikError
. - Wpisz
deleteAllTasks
: wyczyść listę zadań. - 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ń
- Zmień konstruktor
DefaultTaskRepository
+ zApplication
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 }
- Zostały podane zależności, więc 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
, 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
.
- Kliknij prawym przyciskiem myszy nazwę klasy
DefaultTasksRepository
i wybierz Wygeneruj, a następnie Przetestuj. - Postępuj zgodnie z wyświetlanymi instrukcjami, aby utworzyć zbiór
DefaultTasksRepositoryTest
w zestawie źródłowym testu. - 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 }
- Utwórz 3 zmienne, 2 zmienne o członku
FakeDataSource
(po 1 na każde źródło danych w repozytorium) i zmienną dla zmiennejDefaultTasksRepository
, 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
).
- Utwórz metodę o nazwie
createRepository
i dodaj do niej komentarz za pomocą atrybutu@Before
. - Utwórz wystąpienia fałszywych źródeł danych za pomocą list
remoteTasks
ilocalTasks
. - Utwórz instancję
tasksRepository
przy użyciu 2 sfałszowanych źródeł danych iDispatchers.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
!
- Napisz test dla metody
getTasks
repozytorium. Upewnij się, że wywołaniegetTasks
z adresemtrue
(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.
- 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.
- 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. - Wróć do
DefaultTasksRepositoryTest
, dodajrunBlockingTest
, 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))
}
}
- 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
.
- Otwórz
DefaultTasksRepository
i kliknij prawym przyciskiem myszy nazwę zajęć. Następnie wybierz Refaktor -> Wyodrębnij -> Interfejs.
- Wybierz Rozpakuj, aby oddzielić plik.
- W oknie Wyodrębnij interfejs zmień nazwę interfejsu na
TasksRepository
. - W sekcji Members to form Interface (Członkowie w formularzu) zaznacz wszystkich członków oprócz 2 członków i metod prywatnych.
- Kliknij Refaktor. Nowy interfejs
TasksRepository
powinien pojawić się w pakiecie data/source.
Natomiast DefaultTasksRepository
korzysta teraz z TasksRepository
.
- 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
.
- W zbiorze danych test w data/source utwórz plik Kotlin i klasę
FakeTestRepository.kt
, a następnie rozwiń interfejs zTasksRepository
.
FakeTestRepository.kt
class FakeTestRepository : TasksRepository {
}
Wyświetli się komunikat, że musisz wdrożyć metody interfejsu.
- Najedź kursorem na błąd, aż pojawi się menu sugestii, a potem kliknij i wybierz Wdróż członków.
- 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.
- W
FakeTestRepository
dodaj zmiennąLinkedHashMap
reprezentującą bieżącą listę zadań iMutableLiveData
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:
getTasks
– powinna użyć metodytasksServiceData
i zmienić ją w listę przy użyciutasksServiceData.values.toList()
, a następnie zwrócić jako wynikSuccess
.refreshTasks
– aktualizuje wartośćobservableTasks
w taki sposób, aby była zwracana przez funkcjęgetTasks()
.observeTasks
– tworzy regułę przy użyciurunBlocking
i uruchamiarefreshTasks
, a potem zwracaobservableTasks
.
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.
- Dodaj metodę
addTasks
, która pobieravararg
zadań, dodaj każde z nich doHashMap
, 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
.
- Otwórz
TasksViewModel
. - Zmień konstruktor
TasksViewModel
, aby pobierał elementTasksRepository
, 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.
- Na dole pliku
TasksViewModel
poza zajęciami dodaj właściwośćTasksViewModelFactory
w standardowym formacieTasksRepository
.
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.
- 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))
}
- 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.
- Otwórz aplikację
TasksViewModelTest
. - Dodaj właściwość
FakeTestRepository
w elemencieTasksViewModelTest
.
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
, by utworzyćFakeTestRepository
z 3 zadaniami, a następnie utwórztasksViewModel
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)
}
- Nie korzystasz już z kodu AndroidX Test
ApplicationProvider.getApplicationContext
, dlatego możesz też usunąć adnotację@RunWith(AndroidJUnit4::class)
. - 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
).
- Otwórz
TaskDetailViewModel
. - 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 }
- Na dole pliku
TaskDetailViewModel
poza klasą dodajTaskDetailViewModelFactory
.
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)
}
- 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))
}
- 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
- 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 Xkotlinx-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.
- Otwórz
taskdetail.TaskDetailFragment
. - 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łowymtest
).
- Dodaj następujące adnotacje do klasy
TaskDetailFragmentTest
.
TaskDetailsFragmentTest.kt
@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
}
Uwaga:
@MediumTest
– oznacza test jako „średni czas działania” (test testów integracyjnych z@SmallTest
i testy kompleksowe@LargeTest
). Ułatwia to grupowanie i wybór rozmiaru testu.@RunWith(AndroidJUnit4::class)
– używane w klasach przy użyciu Androida X Test.
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
).
- 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:
- Tworzy zadanie.
- Tworzy
Bundle
reprezentujący argumenty fragmentu w zadaniu, które jest przekazywane do fragmentu. - Funkcja
launchFragmentInContainer
tworzy elementFragmentScenario
z tym pakietem i motywem.
To jeszcze nie koniec, ponieważ żaden wynik nie jest potwierdzony. Na razie przeprowadź test i sprawdź, co się stanie.
- Jest to test z instrumentacją, więc upewnij się, że emulator lub urządzenie jest widoczne.
- 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:
- Utwórz klasę Lokalizatora usług, która może skompilować i zapisać repozytorium. Domyślnie kompiluje &repozytorium.
- refaktoryzacja kodu tak, aby w przypadku określonego repozytorium używać Lokalizatora usług;
- 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
.
- Utwórz plik ServiceLocator.kt na najwyższym poziomie głównego zestawu źródeł.
- Określ
object
o nazwieServiceLocator
. - Utwórz zmienne wystąpienia
database
irepository
i ustaw ich wartość nanull
. - 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:
provideTasksRepository
– każde z nich zawiera już utworzone repozytorium lub tworzy nowe. Powinna to być metodasynchronized
w systemiethis
, aby uniknąć sytuacji, w której uruchomiony jest kilka wątków, i przypadkowo przypadkowo zostaną utworzone 2 instancje repozytorium.createTasksRepository
– kod służący do tworzenia nowego repozytorium. Wywoła poleceniecreateTaskLocalDataSource
i utworzy nową grupę:TasksRemoteDataSource
.createTaskLocalDataSource
– kod służący do tworzenia nowego lokalnego źródła danych. Zadzwoni do:createDataBase
.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.
- Na najwyższym poziomie hierarchii pakietów otwórz
TodoApplication
i utwórzval
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
.
- 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
.
- Otwórz
TaskDetailFragement
i znajdź połączenie z numeremgetRepository
u góry klasy. - 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)
}
- 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)
}
- W przypadku
StatisticsViewModel
iAddEditTaskViewModel
zaktualizuj kod, który pobiera repozytorium, aby używać go zTodoApplication
.
TasksFragment.kt
// REPLACE this code
private val tasksRepository = DefaultTasksRepository.getRepository(application)
// WITH this code
private val tasksRepository = (application as TodoApplication).taskRepository
- 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
.
- 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. - Utwórz nową klasę w tym pakiecie źródłowym o nazwie
FakeAndroidTestRepository.kt
. - 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
.
- Otwórz
ServiceLocator.kt
. - 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.
- Dodaj zmienną instancji o nazwie
lock
z wartościąAny
.
ServiceLocator.kt
private val lock = Any()
- 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
.
- Otwórz
TaskDetailFragmentTest
. - Zadeklaruj zmienną
lateinit TasksRepository
. - 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()
}
- Spakuj treść funkcji
activeTaskDetails_DisplayedInUi()
wrunBlockingTest
. - 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)
}
- 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)
}
}
- 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):
- Na urządzeniu testowym otwórz Ustawienia > Opcje programisty.
- 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:
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.
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
.
- Otwórz
TaskDetailFragmentTest.kt
. - 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
- Wszystko po komentarzu
// THEN
używa espresso. Przeanalizuj strukturę testów i zastosowanie znacznikówwithId
, aby zobaczyć, jak powinna wyglądać strona z informacjami. - Uruchom test i sprawdź, czy się on zakończył.
Krok 4. Opcjonalnie: napisz własny test do espresso
Teraz napisz test samodzielnie.
- 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
}
- Patrząc na poprzedni test, ukończ ten test.
- 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
- 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 jakDatePicker
iRecyclerView
. Są tam również testy ułatwień dostępu i zajęcia o nazwieCountingIdlingResource
, które omawiamy później.
Krok 2. Tworzenie testuFragmentów zadań
- Otwórz aplikację
TasksFragment
. - Kliknij prawym przyciskiem myszy nazwę klasy
TasksFragment
i wybierz Wygeneruj, a następnie Przetestuj. Utwórz test w zestawie źródłowym androidTest. - 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
.
- 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)
}
- 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.
- Utwórz nowy sympatyczny fragment
NavController
.
scenario.onFragment {
Navigation.setViewNavController(it.view!!, navController)
}
- 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.
- 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")
)
}
- Uruchom test.
Aby sprawdzić, czy możesz się poruszać, możesz:
- Użyj szablonu Mockito, aby stworzyć przykładową kreację
NavController
. - Załącz przykład do
NavController
. - 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.
- 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.
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:
- Przewodnik po architekturze aplikacji
runBlocking
irunBlockingTest
FragmentScenario
- Espresso
- Mockito
- Jednostka J4
- Biblioteka testowa Androida X
- Podstawowa biblioteka testowa komponentów AndroidX Architecture
- Zbiory źródłowe
- Testowanie z poziomu wiersza poleceń
Materiały wideo:
Inne:
Linki do innych ćwiczeń z programowania znajdziesz w kursie dotyczącym programowania na Androida dla zaawansowanych w Kotlin.