Sposób testowania jednostek lub modułów, które komunikują się z flow zależy od tego, czy testowany podmiot używa przepływu jako danych wejściowych czy wyjściowych.
- Jeśli testowany obiekt obserwuje przepływ, możesz generować przepływy fałszywe zależności, które można kontrolować dzięki testom.
- Jeśli jednostka lub moduł ujawnia przepływ, możesz go odczytać i zweryfikować, wielu elementów emitowanych przez przepływ w teście.
Tworzenie fałszywego producenta
Gdy przedmiot jest konsumentem przepływu, jeden z popularnych sposobów jego przetestowania polega na zastąpieniu producenta fałszywą implementacją. Na przykład, jeśli która obserwuje repozytorium pobierające dane z dwóch źródeł produkcja:
Aby test był deterministyczny, możesz zastąpić repozytorium i jego w fałszywym repozytorium, które zawsze emituje te same fałszywe dane:
Aby emitować wstępnie zdefiniowany ciąg wartości w przepływie, użyj konstruktora flow
:
class MyFakeRepository : MyRepository {
fun observeCount() = flow {
emit(ITEM_1)
}
}
W teście wstrzykiwane jest fałszywe repozytorium, które zastępuje prawdziwe implementacja:
@Test
fun myTest() {
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
...
}
Mając już kontrolę nad wynikami testowanego obiektu, ustal, czy działa prawidłowo, sprawdzając dane wyjściowe.
Potwierdzanie emisji przepływu w teście
Jeśli testowany obiekt ujawnia przepływ, test musi potwierdzić asercję na elementach strumienia danych.
Załóżmy, że repozytorium z poprzedniego przykładu ujawnia przepływ:
W przypadku niektórych testów wystarczy sprawdzić pierwszą emisję lub wartość skończoną liczbę elementów pochodzących z przepływu.
Możesz wykorzystać pierwszą emisję dla przepływu, wywołując funkcję first()
. Ten
funkcja czeka na odbiór pierwszego elementu, a następnie wysyła informację o anulowaniu
do producenta.
@Test
fun myRepositoryTest() = runTest {
// Given a repository that combines values from two data sources:
val repository = MyRepository(fakeSource1, fakeSource2)
// When the repository emits a value
val firstItem = repository.counter.first() // Returns the first item in the flow
// Then check it's the expected item
assertEquals(ITEM_1, firstItem)
}
Jeśli test musi sprawdzić kilka wartości, wywołanie metody toList()
powoduje uruchomienie przepływu.
i poczekać, aż źródło wyemituje wszystkie swoje wartości, a następnie zwrócić je jako
z listy. Ta funkcja działa tylko w przypadku ograniczonych strumieni danych.
@Test
fun myRepositoryTest() = runTest {
// Given a repository with a fake data source that emits ALL_MESSAGES
val messages = repository.observeChatMessages().toList()
// When all messages are emitted then they should be ALL_MESSAGES
assertEquals(ALL_MESSAGES, messages)
}
W przypadku strumieni danych, które wymagają bardziej złożonego zbierania elementów lub nie zwracają
skończonej liczby elementów, możesz wybierać i przekształcać je za pomocą interfejsu API Flow
;
elementy(ów). Oto przykłady:
// Take the second item
outputFlow.drop(1).first()
// Take the first 5 items
outputFlow.take(5).toList()
// Takes the first item verifying that the flow is closed after that
outputFlow.single()
// Finite data streams
// Verify that the flow emits exactly N elements (optional predicate)
outputFlow.count()
outputFlow.count(predicate)
Ciągłe zbieranie danych podczas testu
Gromadzenie procesu za pomocą toList()
, tak jak w poprzednim przykładzie:
collect()
wewnętrznie i została zawieszona do momentu, aż cała lista wyników będzie gotowa
.
Aby przeplatać działania, które powodują, że przepływ generuje wartości i asercje na możesz zbierać wartości z przepływu w trakcie na test.
Na przykład do przetestowania użyj następującej klasy Repository
, a na przykład
towarzyszącej implementacji fałszywego źródła danych z użyciem metody emit
dynamiczne generowanie wartości podczas testu:
class Repository(private val dataSource: DataSource) {
fun scores(): Flow<Int> {
return dataSource.counts().map { it * 10 }
}
}
class FakeDataSource : DataSource {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun counts(): Flow<Int> = flow
}
Używając tej fałszywej treści w teście, możesz utworzyć połączenie kolekcjonerskie, które
stale otrzymuje wartości z Repository
. W tym przykładzie
zbierając je na listę, a następnie stosując asercje do jej treści:
@Test
fun continuouslyCollect() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
val values = mutableListOf<Int>()
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
repository.scores().toList(values)
}
dataSource.emit(1)
assertEquals(10, values[0]) // Assert on the list contents
dataSource.emit(2)
dataSource.emit(3)
assertEquals(30, values[2])
assertEquals(3, values.size) // Assert the number of items collected
}
Ponieważ proces ujawniany przez Repository
w tym miejscu nigdy się nie kończy, toList
które zbiera dane. Rozpoczęcie współprogramu zbierania danych za
TestScope.backgroundScope
gwarantuje anulowanie współprogramu przed zakończeniem testu. W przeciwnym razie
Aplikacja runTest
czekała na zakończenie procesu, co spowodowało zatrzymanie testu
odpowiada, a w końcu kończy się niepowodzeniem.
Zwróć uwagę, jak
UnconfinedTestDispatcher
jest używany do współudziału zbierania danych w tym miejscu. Dzięki temu funkcja zbierania danych
współrzędna jest uruchamiana szybko i jest gotowa do odbierania wartości po launch
„powrót karetki”.
Korzystanie z turbiny
Turbina innej firmy. udostępnia wygodny interfejs API do tworzenia współrzędu kolekcjonerskiego, jako inne udogodnienie dla testowania przepływów:
@Test
fun usingTurbine() = runTest {
val dataSource = FakeDataSource()
val repository = Repository(dataSource)
repository.scores().test {
// Make calls that will trigger value changes only within test{}
dataSource.emit(1)
assertEquals(10, awaitItem())
dataSource.emit(2)
awaitItem() // Ignore items if needed, can also use skip(n)
dataSource.emit(3)
assertEquals(30, awaitItem())
}
}
Zobacz dokumentacji biblioteki dotyczącej .
Testowanie StateFlows
StateFlow
jest obiektem dostrzegalnym
właściciela danych, który może być gromadzony w celu obserwowania wartości posiadanych w czasie,
w postaci strumienia. Zwróć uwagę, że ten strumień wartości jest scalony, co oznacza, że jeśli
są szybko ustawiane w StateFlow
, kolektory tych elementów StateFlow
nie są
wszystkie wartości pośrednie, tylko najnowsze.
Jeśli w testach masz na uwadze konflikt, możesz zebrać wartości parametru StateFlow
możesz zbierać wszystkie inne przepływy, łącznie z trybem Turbine. Próbuję zebrać
i asyst na wszystkich wartościach pośrednich może być pożądane w niektórych scenariuszach testowych.
Zasadniczo zalecamy jednak traktowanie domeny StateFlow
jako właściciela danych i
w standardzie value
. W ten sposób testy sprawdzają bieżący
stanu obiektu w danym punkcie w czasie i nie zależą od tego, czy
które często się ze sobą stykają.
Weźmy na przykład ten model widoku danych, który zbiera wartości z modeli Repository
oraz
ujawnia je w interfejsie StateFlow
:
class MyViewModel(private val myRepository: MyRepository) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()
fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
Fałszywa implementacja elementu Repository
może wyglądać tak:
class FakeRepository : MyRepository {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
Gdy testujesz model ViewModel z użyciem fałszywego modelu, możesz przesyłać wartości
Wyzwalaj aktualizacje w modelu StateFlow
modelu widoku danych, a następnie zatwierdź zaktualizowany
value
:
@Test
fun testHotFakeRepository() = runTest {
val fakeRepository = FakeRepository()
val viewModel = MyViewModel(fakeRepository)
assertEquals(0, viewModel.score.value) // Assert on the initial value
// Start collecting values from the Repository
viewModel.initialize()
// Then we can send in values one by one, which the ViewModel will collect
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value) // Assert on the latest value
}
Praca z StateFlows utworzonymi przez stateIn
W poprzedniej sekcji model widoku danych używa MutableStateFlow
do przechowywania
najnowsza wartość emitowana przez przepływ z Repository
. To częsty schemat,
implementowane zazwyczaj w prostszy sposób za pomocą
stateIn
który zamienia przepływ zimny w ciepły StateFlow
:
class MyViewModelWithStateIn(myRepository: MyRepository) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
Operator stateIn
ma parametr SharingStarted
, który określa, kiedy
staje się aktywny i zaczyna zużywać bazowy przepływ. Opcje takie jak
Często używane są SharingStarted.Lazily
i SharingStarted.WhileSubsribed
w modelach ViewModels.
Nawet jeśli zgłaszasz prośbę do value
elementów StateFlow
w teście,
musisz utworzyć kolektor. Może to być pusty kolektor:
@Test
fun testLazilySharingViewModel() = runTest {
val fakeRepository = HotFakeRepository()
val viewModel = MyViewModelWithStateIn(fakeRepository)
// Create an empty collector for the StateFlow
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.score.collect()
}
assertEquals(0, viewModel.score.value) // Can assert initial value
// Trigger-assert like before
fakeRepository.emit(1)
assertEquals(1, viewModel.score.value)
fakeRepository.emit(2)
fakeRepository.emit(3)
assertEquals(3, viewModel.score.value)
}
Dodatkowe materiały
- Testowanie współprogramów Kotlin na Androidzie
- Procedura Kotlin w Androidzie
StateFlow
iSharedFlow
- Dodatkowe materiały na temat współrzędnych i przepływu Kotlin