Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ wearComposeMaterial3 = "1.5.0-rc02"
wearOngoing = "1.0.0"
wearToolingPreview = "1.0.0"
webkit = "1.14.0"
turbine = "0.13.0"

[libraries]
accompanist-adaptive = "com.google.accompanist:accompanist-adaptive:0.37.3"
Expand Down Expand Up @@ -189,6 +190,7 @@ play-services-wearable = { module = "com.google.android.gms:play-services-wearab
validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" }
wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" }
wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
Expand Down
1 change: 1 addition & 0 deletions kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.junit)
testImplementation(libs.turbine)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.example.flow.test

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@ExperimentalCoroutinesApi
class MainDispatcherRule(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}

override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.example.flow.test

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test

private const val ITEM_1 = "item1"
private val ALL_MESSAGES = listOf("message1", "message2")

interface MyRepository {
fun observeCount(): Flow<String>
fun observeChatMessages(): Flow<List<String>>
}

// [START android_snippets_kotlin_flow_test_fake_repository]
class MyFakeRepository : MyRepository {
override fun observeCount() = flow {
emit(ITEM_1)
}

override fun observeChatMessages(): Flow<List<String>> = flow {
emit(ALL_MESSAGES)
}
}
// [END android_snippets_kotlin_flow_test_fake_repository]

class MyUnitUnderTest(private val myRepository: MyRepository)

class RepositoryTest {

@Test
fun myTest() {
// [START android_snippets_kotlin_flow_test_my_test]
// Given a class with fake dependencies:
val sut = MyUnitUnderTest(MyFakeRepository())
// Trigger and verify
// ...
// [END android_snippets_kotlin_flow_test_my_test]
}

@Test
fun myRepositoryTest_first() = runTest {
// [START android_snippets_kotlin_flow_test_repository_test_first]
// Given a repository that combines values from two data sources:
val repository = MyFakeRepository()

// When the repository emits a value
val firstItem = repository.observeCount().first() // Returns the first item in the flow

// Then check it's the expected item
assertEquals(ITEM_1, firstItem)
// [END android_snippets_kotlin_flow_test_repository_test_first]
}

@Test
fun myRepositoryTest_toList() = runTest {
// [START android_snippets_kotlin_flow_test_repository_test_to_list]
// Given a repository with a fake data source that emits ALL_MESSAGES
val repository = MyFakeRepository()
val messages = repository.observeChatMessages().toList()

// When all messages are emitted then they should be ALL_MESSAGES
assertEquals(ALL_MESSAGES, messages[0])
// [END android_snippets_kotlin_flow_test_repository_test_to_list]
}
}
44 changes: 44 additions & 0 deletions kotlin/src/test/java/com/android/example/flow/test/TurbineTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.example.flow.test

import app.cash.turbine.test
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test

class TurbineTest {
@Test
fun usingTurbine() = runTest {
// [START android_snippets_kotlin_flow_test_using_turbine]
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())
}
// [END android_snippets_kotlin_flow_test_using_turbine]
}
}
126 changes: 126 additions & 0 deletions kotlin/src/test/java/com/android/example/flow/test/ViewModelTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.example.flow.test

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

interface MyRepositoryVM {
fun scores(): Flow<Int>
}

// [START android_snippets_kotlin_flow_test_my_view_model]
class MyViewModel(private val myRepository: MyRepositoryVM) : ViewModel() {
private val _score = MutableStateFlow(0)
val score: StateFlow<Int> = _score.asStateFlow()

fun initialize() {
viewModelScope.launch {
myRepository.scores().collect { score ->
_score.value = score
}
}
}
}
// [END android_snippets_kotlin_flow_test_my_view_model]

// [START android_snippets_kotlin_flow_test_fake_repository_viewmodel]
class FakeRepository : MyRepositoryVM {
private val flow = MutableSharedFlow<Int>()
suspend fun emit(value: Int) = flow.emit(value)
override fun scores(): Flow<Int> = flow
}
// [END android_snippets_kotlin_flow_test_fake_repository_viewmodel]

@OptIn(ExperimentalCoroutinesApi::class)
class ViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()

// [START android_snippets_kotlin_flow_test_hot_fake_repository]
@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
}
// [END android_snippets_kotlin_flow_test_hot_fake_repository]
}

// [START android_snippets_kotlin_flow_test_my_view_model_with_state_in]
class MyViewModelWithStateIn(myRepository: MyRepositoryVM) : ViewModel() {
val score: StateFlow<Int> = myRepository.scores()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0)
}
// [END android_snippets_kotlin_flow_test_my_view_model_with_state_in]

typealias HotFakeRepository = FakeRepository

@OptIn(ExperimentalCoroutinesApi::class)
class LazilySharingViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()

// [START android_snippets_kotlin_flow_test_lazily_sharing_view_model]
@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)
}
// [END android_snippets_kotlin_flow_test_lazily_sharing_view_model]
}
39 changes: 39 additions & 0 deletions kotlin/src/test/java/com/android/example/flow/test/fakes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.example.flow.test

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map

// [START android_snippets_kotlin_flow_test_repository_and_datasource]
interface DataSource {
fun counts(): Flow<Int>
}

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
}
// [END android_snippets_kotlin_flow_test_repository_and_datasource]