mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-02 03:36:46 +01:00
chore: add test in core-navigation (#558)
This commit is contained in:
parent
97531b5d4b
commit
0e314b24da
102
CONTRIBUTING.md
102
CONTRIBUTING.md
@ -343,4 +343,104 @@ As far as Compose code is concerned, we take Google’s indications as a baselin
|
||||
|
||||
### 6.3 Test structure
|
||||
|
||||
TBD
|
||||
#### 6.3.1 Unit tests
|
||||
|
||||
Unit test are targeted as a single unit of code: the test class will have the same name of the
|
||||
component under test, followed by the `Test` suffix and will be placed in the same package, within
|
||||
the `androidUnitTest` source set. The tests will be platform specific for now, since multi-platform
|
||||
tests under the `commonTest` set require some additional setup and a considerable amount of extra
|
||||
effort.
|
||||
|
||||
For every subject under test (SUT), the dependencies will be doubled using test mocks created using
|
||||
the `mockk` library, and the assertions on flows and channels will be made using the `turbine`
|
||||
library.
|
||||
|
||||
Each test class will contain at least one method annotated with `@Test` and, especially if it
|
||||
contains suspending functions, its body will be wrapped in a `runTest` scope function (better to
|
||||
always include it). In order for suspending functions to be called in the correct coroutine
|
||||
context (where the main thread of Android is replaced by an `Unconfined` dispatcher), the JUnit rule
|
||||
`DispatcherTestRule` defined in :core:testutils should be included and annotated with `@get:Rule`.
|
||||
|
||||
Each test method shall consider a single interaction (W) on the SUT that happens in a
|
||||
precondition (G) and should produce a result (T) against which some assertions will be performed.
|
||||
|
||||
These three elements will be reflected in the method name, which shall have the GWT form, i.e.:
|
||||
```givenX_whenY_thenZ```
|
||||
|
||||
The body of the method will be therefore divided into three parts (separated by a blank line):
|
||||
|
||||
- (optional) precondition: mock setup with predefined answers to invocations;
|
||||
- interaction: the method/property to test will be invoked/interacted with on the SUT;
|
||||
- result: one or more assertions on the result got in the previous step; optionally this part will
|
||||
also contains some verification on the mocks/spies to make sure the proper interactions have or
|
||||
have not happened according to the expectations in the previous step.
|
||||
|
||||
Reference unit test:
|
||||
|
||||
```kotlin
|
||||
class DefaultNavigationCoordinatorTest {
|
||||
|
||||
// coroutine test rule (must be a public property)
|
||||
@get:Rule
|
||||
val dispatcherRule = DispatcherTestRule()
|
||||
|
||||
// subject under test
|
||||
private val sut = DefaultNavigationCoordinator()
|
||||
|
||||
// test method (1)
|
||||
@Test
|
||||
fun givenNavigatorCanPop_whenRootNavigatorSet_thenCanPopIsUpdated() = runTest {
|
||||
// setup
|
||||
val navigator = mockk<Navigator> {
|
||||
every { canPop } returns true
|
||||
}
|
||||
|
||||
// interaction
|
||||
sut.setRootNavigator(navigator)
|
||||
|
||||
// assertions
|
||||
val value = sut.canPop.value
|
||||
assertTrue(value)
|
||||
}
|
||||
|
||||
// test method (2)
|
||||
@Test
|
||||
fun whenChangeTab_thenCurrentTabIsUpdated() = runTest {
|
||||
// setup with capturing slots
|
||||
val tabSlot = slot<Tab>()
|
||||
val navigator = mockk<TabNavigator>(relaxUnitFun = true) {
|
||||
every { current = capture(tabSlot) } answers {}
|
||||
}
|
||||
val tab = object : Tab {
|
||||
override val options @Composable get() = TabOptions(index = 0u, "title")
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
sut.setTabNavigator(navigator)
|
||||
|
||||
// interaction
|
||||
sut.changeTab(tab)
|
||||
|
||||
// assertions
|
||||
val value = tabSlot.captured
|
||||
assertEquals(tab, value)
|
||||
}
|
||||
|
||||
// test method (3)
|
||||
@Test
|
||||
fun whenSubmitDeeplink_thenValueIsEmitted() = runTest {
|
||||
val url = "deeplink-url"
|
||||
// interaction
|
||||
sut.submitDeeplink(url)
|
||||
|
||||
// assertions on the flow with turbine's test extensions
|
||||
sut.deepLinkUrl.test {
|
||||
val value = awaitItem()
|
||||
assertEquals(url, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -43,8 +43,14 @@ kotlin {
|
||||
implementation(projects.core.persistence)
|
||||
implementation(projects.domain.lemmy.data)
|
||||
}
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
val androidUnitTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(kotlin("test-junit"))
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.turbine)
|
||||
implementation(projects.core.testutils)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.navigation
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.testutils.DispatcherTestRule
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DefaultDrawerCoordinatorTest {
|
||||
|
||||
@get:Rule
|
||||
val dispatcherRule = DispatcherTestRule()
|
||||
|
||||
private val sut = DefaultDrawerCoordinator()
|
||||
|
||||
@Test
|
||||
fun whenToggled_thenEventIsEmitted() = runTest {
|
||||
launch {
|
||||
sut.toggleDrawer()
|
||||
}
|
||||
|
||||
sut.events.test {
|
||||
val evt = awaitItem()
|
||||
assertEquals(DrawerEvent.Toggle, evt)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenClosed_thenEventIsEmitted() = runTest {
|
||||
launch {
|
||||
sut.closeDrawer()
|
||||
}
|
||||
|
||||
sut.events.test {
|
||||
val evt = awaitItem()
|
||||
assertEquals(DrawerEvent.Close, evt)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSetGesturesEnabled_thenStateIsUpdated() = runTest {
|
||||
val initial = sut.gesturesEnabled.value
|
||||
assertTrue(initial)
|
||||
|
||||
sut.setGesturesEnabled(false)
|
||||
|
||||
val value = sut.gesturesEnabled.value
|
||||
assertFalse(value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSendEvent_thenEventIsEmitted() = runTest {
|
||||
val community = CommunityModel(id = 0, name = "test")
|
||||
launch {
|
||||
sut.sendEvent(DrawerEvent.OpenCommunity(community))
|
||||
}
|
||||
sut.events.test {
|
||||
val evt = awaitItem()
|
||||
assertEquals(DrawerEvent.OpenCommunity(community), evt)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,235 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.cash.turbine.test
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.core.screen.ScreenKey
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
import cafe.adriel.voyager.navigator.bottomSheet.BottomSheetNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.testutils.DispatcherTestRule
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DefaultNavigationCoordinatorTest {
|
||||
|
||||
@get:Rule
|
||||
val dispatcherRule = DispatcherTestRule()
|
||||
|
||||
private val sut = DefaultNavigationCoordinator()
|
||||
|
||||
@Test
|
||||
fun whenSetCurrentSection_thenValueIsUpdated() = runTest {
|
||||
val initial = sut.currentSection.value
|
||||
assertNull(initial)
|
||||
|
||||
sut.setCurrentSection(TabNavigationSection.Settings)
|
||||
|
||||
val value = sut.currentSection.value
|
||||
assertEquals(TabNavigationSection.Settings, value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSetCurrentSectionTwice_thenOnDoubleTabSelectionTriggered() = runTest {
|
||||
sut.setCurrentSection(TabNavigationSection.Profile)
|
||||
launch {
|
||||
delay(250)
|
||||
sut.setCurrentSection(TabNavigationSection.Profile)
|
||||
}
|
||||
sut.onDoubleTabSelection.test {
|
||||
val section = awaitItem()
|
||||
assertEquals(TabNavigationSection.Profile, section)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNavigatorCanPop_whenRootNavigatorSet_thenCanPopIsUpdated() = runTest {
|
||||
val initial = sut.canPop.value
|
||||
assertFalse(initial)
|
||||
val navigator = mockk<Navigator> {
|
||||
every { canPop } returns true
|
||||
}
|
||||
|
||||
sut.setRootNavigator(navigator)
|
||||
|
||||
val value = sut.canPop.value
|
||||
assertTrue(value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSubmitDeeplink_thenValueIsEmitted() = runTest {
|
||||
val url = "deeplink-url"
|
||||
sut.submitDeeplink(url)
|
||||
|
||||
sut.deepLinkUrl.test {
|
||||
val value = awaitItem()
|
||||
assertEquals(url, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSetInboxUnread_thenValueIsUpdated() = runTest {
|
||||
val count = 5
|
||||
sut.setInboxUnread(count)
|
||||
val value = sut.inboxUnread.value
|
||||
assertEquals(count, value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenSetExitMessageVisible_thenValueIsUpdated() = runTest {
|
||||
val initial = sut.exitMessageVisible.value
|
||||
assertFalse(initial)
|
||||
|
||||
sut.setExitMessageVisible(true)
|
||||
val value = sut.exitMessageVisible.value
|
||||
assertTrue(value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenChangeTab_thenCurrentTabIsUpdated() = runTest {
|
||||
val tabSlot = slot<Tab>()
|
||||
val navigator = mockk<TabNavigator>(relaxUnitFun = true) {
|
||||
every { current = capture(tabSlot) } answers {}
|
||||
}
|
||||
val tab = object : Tab {
|
||||
override val options @Composable get() = TabOptions(index = 0u, "title")
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
sut.setTabNavigator(navigator)
|
||||
|
||||
sut.changeTab(tab)
|
||||
|
||||
val value = tabSlot.captured
|
||||
assertEquals(tab, value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenPushScreen_thenInteractionsAreAsExpected() = runTest {
|
||||
val previous = object : Screen {
|
||||
|
||||
override val key: ScreenKey = "old"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
val screen = object : Screen {
|
||||
|
||||
override val key: ScreenKey = "new"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
val navigator = mockk<Navigator>(relaxUnitFun = true) {
|
||||
every { canPop } returns true
|
||||
every { lastItem } returns previous
|
||||
}
|
||||
sut.setRootNavigator(navigator)
|
||||
|
||||
sut.pushScreen(screen)
|
||||
|
||||
val canPop = sut.canPop.value
|
||||
assertTrue(canPop)
|
||||
verify {
|
||||
navigator.push(screen)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenPushScreenTwice_thenInteractionsAreAsExpected() = runTest {
|
||||
val screen = object : Screen {
|
||||
|
||||
override val key: ScreenKey = ""
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
val navigator = mockk<Navigator>(relaxUnitFun = true) {
|
||||
every { canPop } returns true
|
||||
every { lastItem } returns screen
|
||||
}
|
||||
sut.setRootNavigator(navigator)
|
||||
|
||||
sut.pushScreen(screen)
|
||||
|
||||
val canPop = sut.canPop.value
|
||||
assertTrue(canPop)
|
||||
verify(inverse = true) {
|
||||
navigator.push(screen)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenPopScreen_thenInteractionsAreAsExpected() = runTest {
|
||||
val navigator = mockk<Navigator>(relaxUnitFun = true) {
|
||||
every { pop() } returns true
|
||||
every { canPop } returns false
|
||||
}
|
||||
sut.setRootNavigator(navigator)
|
||||
|
||||
sut.popScreen()
|
||||
|
||||
val canPop = sut.canPop.value
|
||||
assertFalse(canPop)
|
||||
verify {
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenShowBottomSheet_thenInteractionsAreAsExpected() = runTest {
|
||||
val screen = object : Screen {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Box(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
val navigator = mockk<BottomSheetNavigator>(relaxUnitFun = true)
|
||||
sut.setBottomNavigator(navigator)
|
||||
|
||||
sut.showBottomSheet(screen)
|
||||
|
||||
verify {
|
||||
navigator.show(screen)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun whenHideBottomSheet_thenInteractionsAreAsExpected() = runTest {
|
||||
val navigator = mockk<BottomSheetNavigator>(relaxUnitFun = true) {
|
||||
every { isVisible } returns true
|
||||
}
|
||||
sut.setBottomNavigator(navigator)
|
||||
|
||||
sut.hideBottomSheet()
|
||||
|
||||
verify {
|
||||
navigator.hide()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,11 +11,11 @@ import org.junit.runner.Description
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DispatcherTestRule(
|
||||
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
|
||||
) : TestWatcher() {
|
||||
|
||||
override fun starting(description: Description) {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description) {
|
||||
|
@ -27,6 +27,7 @@ multiplatform_settings = "1.1.1"
|
||||
reorderable = "1.3.1"
|
||||
sqlcipher = "4.5.5"
|
||||
sqldelight = "2.0.1"
|
||||
turbine = "1.0.0"
|
||||
voyager = "1.0.0"
|
||||
|
||||
android_targetSdk = "34"
|
||||
@ -92,6 +93,7 @@ exoplayer_ui = { module = "androidx.media3:media3-ui", version.ref = "androidx.m
|
||||
reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
|
||||
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
|
||||
|
||||
[plugins]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user