diff --git a/build-logic/convention/src/main/kotlin/com/example/architecture/convention/DomainModuleConventionPlugin.kt b/build-logic/convention/src/main/kotlin/com/example/architecture/convention/DomainModuleConventionPlugin.kt index ebe663b..6318f88 100644 --- a/build-logic/convention/src/main/kotlin/com/example/architecture/convention/DomainModuleConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/com/example/architecture/convention/DomainModuleConventionPlugin.kt @@ -20,6 +20,8 @@ class DomainModuleConventionPlugin : Plugin { add("implementation", libs.findLibrary("kotlinx-coroutines-core").get()) add("testImplementation", libs.findLibrary("junit-jupiter-api").get()) add("testImplementation", libs.findLibrary("assertk").get()) + // Domain doesn't consume the `unit-test` bundle, so MockK is added explicitly here. + add("testImplementation", libs.findLibrary("mockk").get()) add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get()) // Gradle 9 dropped the bundled launcher; JUnit 5 won't start without it. add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get()) diff --git a/feature/characters/domain/src/test/kotlin/com/example/architecture/feature/characters/domain/usecase/GetCharactersPageUseCaseTest.kt b/feature/characters/domain/src/test/kotlin/com/example/architecture/feature/characters/domain/usecase/GetCharactersPageUseCaseTest.kt index 637ceab..aee640b 100644 --- a/feature/characters/domain/src/test/kotlin/com/example/architecture/feature/characters/domain/usecase/GetCharactersPageUseCaseTest.kt +++ b/feature/characters/domain/src/test/kotlin/com/example/architecture/feature/characters/domain/usecase/GetCharactersPageUseCaseTest.kt @@ -7,23 +7,29 @@ import com.example.architecture.core.domain.DataError import com.example.architecture.core.domain.Result import com.example.architecture.feature.characters.domain.CharacterRepository import com.example.architecture.feature.characters.domain.model.Character -import com.example.architecture.feature.characters.domain.model.CharacterDetails import com.example.architecture.feature.characters.domain.model.CharacterStatus import com.example.architecture.feature.characters.domain.model.CharactersPage +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test /** * Tests for the (thin pass-through) [GetCharactersPageUseCase]: it must forward the requested page to * the repository and return its result verbatim — success and error alike. Pure JVM test on the - * JUnit 5 platform (see DomainModuleConventionPlugin); collaborator is a hand-written fake. + * JUnit 5 platform (see DomainModuleConventionPlugin); the [CharacterRepository] collaborator is a + * MockK mock, stubbed with `coEvery` and verified with `coVerify`. */ class GetCharactersPageUseCaseTest { + private val repository = mockk() + private val useCase = GetCharactersPageUseCase(repository) + @Test fun `returns the repository page on success`() = runBlocking { val page = CharactersPage(characters = listOf(domainCharacter(1)), nextPage = 2) - val useCase = GetCharactersPageUseCase(FakeCharacterRepository(pageResult = Result.Success(page))) + coEvery { repository.getCharacters(1) } returns Result.Success(page) val result = useCase(page = 1) @@ -32,9 +38,7 @@ class GetCharactersPageUseCaseTest { @Test fun `propagates the repository error`() = runBlocking { - val useCase = GetCharactersPageUseCase( - FakeCharacterRepository(pageResult = Result.Error(DataError.Network.SERVER_ERROR)), - ) + coEvery { repository.getCharacters(1) } returns Result.Error(DataError.Network.SERVER_ERROR) val result = useCase(page = 1) @@ -44,29 +48,12 @@ class GetCharactersPageUseCaseTest { @Test fun `forwards the requested page number`() = runBlocking { - val fake = FakeCharacterRepository( - pageResult = Result.Success(CharactersPage(characters = emptyList(), nextPage = null)), - ) - val useCase = GetCharactersPageUseCase(fake) + coEvery { repository.getCharacters(any()) } returns + Result.Success(CharactersPage(characters = emptyList(), nextPage = null)) useCase(page = 7) - assertThat(fake.lastRequestedPage).isEqualTo(7) - } - - private class FakeCharacterRepository( - private val pageResult: Result, - ) : CharacterRepository { - var lastRequestedPage: Int? = null - private set - - override suspend fun getCharacters(page: Int): Result { - lastRequestedPage = page - return pageResult - } - - override suspend fun getCharacterDetails(id: Int): Result = - Result.Error(DataError.Network.NOT_FOUND) + coVerify(exactly = 1) { repository.getCharacters(7) } } private fun domainCharacter(id: Int) = Character( diff --git a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterDetailViewModelTest.kt b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterDetailViewModelTest.kt index aefc690..7731c9d 100644 --- a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterDetailViewModelTest.kt +++ b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterDetailViewModelTest.kt @@ -9,6 +9,10 @@ import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import com.example.architecture.core.domain.DataError +import com.example.architecture.core.domain.Result +import com.example.architecture.feature.characters.domain.CharacterRepository +import io.mockk.coEvery +import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -24,14 +28,15 @@ import org.junit.jupiter.api.assertThrows /** * Unit tests for [CharacterDetailViewModel]. The character id arrives via [SavedStateHandle] (written * by type-safe navigation), which is constructed directly here — proving the VM needs no navigation - * dependency. Collaborator is a [FakeCharacterRepository]; assertions use AssertK; the back event is - * observed with Turbine. + * dependency. The [CharacterRepository] collaborator is a *relaxed* MockK mock, so the "missing id" + * case needs no stubbing while the rest stub `getCharacterDetails` explicitly with `coEvery`; + * assertions use AssertK; the back event is observed with Turbine. */ @OptIn(ExperimentalCoroutinesApi::class) class CharacterDetailViewModelTest { private val dispatcher = StandardTestDispatcher() - private val repository = FakeCharacterRepository() + private val repository = mockk(relaxed = true) @BeforeEach fun setUp() { @@ -48,7 +53,7 @@ class CharacterDetailViewModelTest { @Test fun `loads details on init`() = runTest(dispatcher.scheduler) { - repository.setDetails(characterDetails(1)) + coEvery { repository.getCharacterDetails(1) } returns Result.Success(characterDetails(1)) val viewModel = viewModel(characterId = 1) advanceUntilIdle() @@ -62,7 +67,7 @@ class CharacterDetailViewModelTest { @Test fun `load failure surfaces an error and no details`() = runTest(dispatcher.scheduler) { - repository.failWith = DataError.Network.SERVER_ERROR + coEvery { repository.getCharacterDetails(1) } returns Result.Error(DataError.Network.SERVER_ERROR) val viewModel = viewModel(characterId = 1) advanceUntilIdle() @@ -75,14 +80,13 @@ class CharacterDetailViewModelTest { @Test fun `retry after a failure clears the error and loads details`() = runTest(dispatcher.scheduler) { - repository.failWith = DataError.Network.NO_INTERNET + coEvery { repository.getCharacterDetails(1) } returns Result.Error(DataError.Network.NO_INTERNET) val viewModel = viewModel(characterId = 1) advanceUntilIdle() assertThat(viewModel.state.value.error).isNotNull() - // The next attempt will succeed. - repository.failWith = null - repository.setDetails(characterDetails(1)) + // Same call, new answer — the latest `coEvery` wins, so the retry attempt succeeds. + coEvery { repository.getCharacterDetails(1) } returns Result.Success(characterDetails(1)) viewModel.onAction(CharacterDetailAction.OnRetry) advanceUntilIdle() @@ -93,7 +97,7 @@ class CharacterDetailViewModelTest { @Test fun `back click emits NavigateBack`() = runTest(dispatcher.scheduler) { - repository.setDetails(characterDetails(1)) + coEvery { repository.getCharacterDetails(1) } returns Result.Success(characterDetails(1)) val viewModel = viewModel(characterId = 1) advanceUntilIdle() @@ -108,6 +112,7 @@ class CharacterDetailViewModelTest { @Test fun `missing character id fails fast`() { // The route contract: type-safe nav must have written characterId into SavedStateHandle. + // The constructor throws before the (relaxed) repository is ever touched. assertThrows { CharacterDetailViewModel(SavedStateHandle(), repository) } diff --git a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterFixtures.kt b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterFixtures.kt new file mode 100644 index 0000000..eabdf90 --- /dev/null +++ b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterFixtures.kt @@ -0,0 +1,28 @@ +package com.example.architecture.feature.characters.presentation + +import com.example.architecture.feature.characters.domain.model.Character +import com.example.architecture.feature.characters.domain.model.CharacterDetails +import com.example.architecture.feature.characters.domain.model.CharacterStatus + +/** Minimal list-item domain fixture for presentation tests. */ +fun character(id: Int): Character = Character( + id = id, + name = "Character $id", + status = CharacterStatus.ALIVE, + species = "Human", + imageUrl = "https://example.com/$id.png", +) + +/** Minimal detail domain fixture for presentation tests. */ +fun characterDetails(id: Int): CharacterDetails = CharacterDetails( + id = id, + name = "Character $id", + status = CharacterStatus.ALIVE, + species = "Human", + type = "Genetic experiment", + gender = "Male", + origin = "Earth (C-137)", + location = "Citadel of Ricks", + imageUrl = "https://example.com/$id.png", + episodeCount = 10, +) diff --git a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterListViewModelTest.kt b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterListViewModelTest.kt index cecd9aa..9ff0b02 100644 --- a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterListViewModelTest.kt +++ b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/CharacterListViewModelTest.kt @@ -13,7 +13,14 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import com.example.architecture.core.domain.DataError +import com.example.architecture.core.domain.Result +import com.example.architecture.feature.characters.domain.CharacterRepository +import com.example.architecture.feature.characters.domain.model.CharactersPage import com.example.architecture.feature.characters.domain.usecase.GetCharactersPageUseCase +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -31,14 +38,15 @@ import org.junit.jupiter.api.Test * * Uses [StandardTestDispatcher] (not Unconfined) so launched work is queued until `advanceUntilIdle`, * which lets the duplicate-paging test observe the *synchronous* loading-flag guard before any - * coroutine runs. Collaborator is a [FakeCharacterRepository] (a fake, not a mock); `state`/`events` - * are observed with Turbine; assertions use AssertK. + * coroutine runs. The collaborator is a MockK mock of [CharacterRepository] (the real + * [GetCharactersPageUseCase] wraps it), stubbed per page with `coEvery` and verified with `coVerify`; + * `state`/`events` are observed with Turbine; assertions use AssertK. */ @OptIn(ExperimentalCoroutinesApi::class) class CharacterListViewModelTest { private val dispatcher = StandardTestDispatcher() - private val repository = FakeCharacterRepository() + private val repository = mockk() @BeforeEach fun setUp() { @@ -55,7 +63,8 @@ class CharacterListViewModelTest { @Test fun `loads the first page on init`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1), character(2)), nextPage = 2) + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1), character(2)), nextPage = 2)) val viewModel = viewModel() @@ -78,7 +87,7 @@ class CharacterListViewModelTest { @Test fun `initial load failure emits a snackbar event and a full-screen error`() = runTest(dispatcher.scheduler) { - repository.failWith = DataError.Network.NO_INTERNET + coEvery { repository.getCharacters(any()) } returns Result.Error(DataError.Network.NO_INTERNET) val viewModel = viewModel() @@ -95,8 +104,10 @@ class CharacterListViewModelTest { @Test fun `does not load past the last page`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1)), nextPage = 2) - repository.setPage(page = 2, characters = listOf(character(2)), nextPage = null) // last page + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1)), nextPage = 2)) + coEvery { repository.getCharacters(2) } returns + Result.Success(CharactersPage(listOf(character(2)), nextPage = null)) // last page val viewModel = viewModel() advanceUntilIdle() // init → page 1 @@ -107,21 +118,24 @@ class CharacterListViewModelTest { assertThat(viewModel.state.value).prop(CharacterListState::endReached).isTrue() assertThat(viewModel.state.value).prop(CharacterListState::characters).hasSize(2) - val callsBefore = repository.getCharactersCallCount viewModel.onAction(CharacterListAction.OnLoadNextPage) advanceUntilIdle() // guarded by endReached → no request - assertThat(repository.getCharactersCallCount).isEqualTo(callsBefore) + // Page 2 was fetched exactly once and no page 3 was ever requested (a page-3 fetch would also + // blow up the strict mock as an unstubbed call). + coVerify(exactly = 1) { repository.getCharacters(2) } + coVerify(exactly = 0) { repository.getCharacters(3) } } @Test fun `rapid duplicate next-page actions load the page only once`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1)), nextPage = 2) - repository.setPage(page = 2, characters = listOf(character(2)), nextPage = 3) + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1)), nextPage = 2)) + coEvery { repository.getCharacters(2) } returns + Result.Success(CharactersPage(listOf(character(2)), nextPage = 3)) val viewModel = viewModel() advanceUntilIdle() // init → page 1 - val callsBefore = repository.getCharactersCallCount // Both fire before any launched coroutine runs; the second sees the synchronously-set // isLoadingNextPage flag and is guarded out. @@ -129,14 +143,15 @@ class CharacterListViewModelTest { viewModel.onAction(CharacterListAction.OnLoadNextPage) advanceUntilIdle() - assertThat(repository.getCharactersCallCount).isEqualTo(callsBefore + 1) + coVerify(exactly = 1) { repository.getCharacters(2) } assertThat(viewModel.state.value).prop(CharacterListState::currentPage).isEqualTo(2) } @Test fun `ignores a next-page request while the initial load is in flight`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1)), nextPage = 2) + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1)), nextPage = 2)) val viewModel = viewModel() // restore() set isLoading = true synchronously; its coroutine hasn't run yet, so this @@ -145,12 +160,13 @@ class CharacterListViewModelTest { advanceUntilIdle() // Only the single initial load ran — the guarded next-page request never fired. - assertThat(repository.getCharactersCallCount).isEqualTo(1) + coVerify(exactly = 1) { repository.getCharacters(1) } + coVerify(exactly = 0) { repository.getCharacters(2) } } @Test fun `retry after a failed initial load rebuilds the list`() = runTest(dispatcher.scheduler) { - repository.failWith = DataError.Network.NO_INTERNET + coEvery { repository.getCharacters(any()) } returns Result.Error(DataError.Network.NO_INTERNET) val viewModel = viewModel() viewModel.events.test { @@ -160,9 +176,10 @@ class CharacterListViewModelTest { assertThat(awaitItem()).isInstanceOf(CharacterListEvent.ShowSnackbar::class) assertThat(viewModel.state.value).prop(CharacterListState::characters).isEmpty() - // Empty branch of retry(): the repository recovers, OnRetry rebuilds from page 1. - repository.failWith = null - repository.setPage(page = 1, characters = listOf(character(1), character(2)), nextPage = 2) + // Empty branch of retry(): the repository recovers (the later, more specific stub for + // page 1 wins over the `any()` failure), OnRetry rebuilds from page 1. + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1), character(2)), nextPage = 2)) viewModel.onAction(CharacterListAction.OnRetry) advanceUntilIdle() cancelAndIgnoreRemainingEvents() @@ -174,12 +191,14 @@ class CharacterListViewModelTest { @Test fun `retry after a failed next page re-requests that page`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1)), nextPage = 2) + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1)), nextPage = 2)) val viewModel = viewModel() advanceUntilIdle() // page 1 loaded (no event) viewModel.events.test { - // Page 2 isn't configured yet → next-page load fails; list keeps page 1, shows an error. + // Page 2 fails on the first attempt → list keeps page 1, shows an error. + coEvery { repository.getCharacters(2) } returns Result.Error(DataError.Network.NOT_FOUND) viewModel.onAction(CharacterListAction.OnLoadNextPage) advanceUntilIdle() assertThat(awaitItem()).isInstanceOf(CharacterListEvent.ShowSnackbar::class) @@ -187,7 +206,8 @@ class CharacterListViewModelTest { // Non-empty branch of retry(): with page 2 now available, OnRetry re-requests page 2 and // appends it (currentPage stayed 1 because loadPage only advances on success). - repository.setPage(page = 2, characters = listOf(character(2)), nextPage = null) + coEvery { repository.getCharacters(2) } returns + Result.Success(CharactersPage(listOf(character(2)), nextPage = null)) viewModel.onAction(CharacterListAction.OnRetry) advanceUntilIdle() cancelAndIgnoreRemainingEvents() @@ -196,12 +216,16 @@ class CharacterListViewModelTest { assertThat(viewModel.state.value).prop(CharacterListState::characters).hasSize(2) assertThat(viewModel.state.value).prop(CharacterListState::currentPage).isEqualTo(2) assertThat(viewModel.state.value).prop(CharacterListState::error).isNull() + // Page 2 was requested twice: the failed first load and the successful retry. + coVerify(exactly = 2) { repository.getCharacters(2) } } @Test fun `restores up to the saved page after process death`() = runTest(dispatcher.scheduler) { - repository.setPage(page = 1, characters = listOf(character(1)), nextPage = 2) - repository.setPage(page = 2, characters = listOf(character(2)), nextPage = 3) + coEvery { repository.getCharacters(1) } returns + Result.Success(CharactersPage(listOf(character(1)), nextPage = 2)) + coEvery { repository.getCharacters(2) } returns + Result.Success(CharactersPage(listOf(character(2)), nextPage = 3)) // Navigation/SavedStateHandle persisted the last loaded page across process death. val savedStateHandle = SavedStateHandle(mapOf("currentPage" to 2)) @@ -211,5 +235,9 @@ class CharacterListViewModelTest { // Both pages are rebuilt (1 then 2), and currentPage is restored. assertThat(viewModel.state.value).prop(CharacterListState::characters).hasSize(2) assertThat(viewModel.state.value).prop(CharacterListState::currentPage).isEqualTo(2) + coVerifyOrder { + repository.getCharacters(1) + repository.getCharacters(2) + } } } diff --git a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/FakeCharacterRepository.kt b/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/FakeCharacterRepository.kt deleted file mode 100644 index 683a352..0000000 --- a/feature/characters/presentation/src/test/kotlin/com/example/architecture/feature/characters/presentation/FakeCharacterRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.architecture.feature.characters.presentation - -import com.example.architecture.core.domain.DataError -import com.example.architecture.core.domain.Result -import com.example.architecture.feature.characters.domain.CharacterRepository -import com.example.architecture.feature.characters.domain.model.Character -import com.example.architecture.feature.characters.domain.model.CharacterDetails -import com.example.architecture.feature.characters.domain.model.CharacterStatus -import com.example.architecture.feature.characters.domain.model.CharactersPage - -/** - * In-memory [CharacterRepository] for ViewModel tests — a **fake**, not a mock: it has real behaviour - * (returns configured pages/details, counts calls, can be flipped to fail) so tests assert against a - * working collaborator instead of recording interactions. Configure pages via [setPage]/[setDetails]; - * set [failWith] to make every call fail with a specific [DataError]. - */ -class FakeCharacterRepository : CharacterRepository { - - /** When non-null, every call fails with this error (overrides any configured data). */ - var failWith: DataError? = null - - var getCharactersCallCount = 0 - private set - var getCharacterDetailsCallCount = 0 - private set - - private val pages = mutableMapOf() - private val details = mutableMapOf() - - fun setPage(page: Int, characters: List, nextPage: Int?) { - pages[page] = CharactersPage(characters = characters, nextPage = nextPage) - } - - fun setDetails(value: CharacterDetails) { - details[value.id] = value - } - - override suspend fun getCharacters(page: Int): Result { - getCharactersCallCount++ - failWith?.let { return Result.Error(it) } - val pageData = pages[page] ?: return Result.Error(DataError.Network.NOT_FOUND) - return Result.Success(pageData) - } - - override suspend fun getCharacterDetails(id: Int): Result { - getCharacterDetailsCallCount++ - failWith?.let { return Result.Error(it) } - val value = details[id] ?: return Result.Error(DataError.Network.NOT_FOUND) - return Result.Success(value) - } -} - -/** Minimal list-item domain fixture. */ -fun character(id: Int): Character = Character( - id = id, - name = "Character $id", - status = CharacterStatus.ALIVE, - species = "Human", - imageUrl = "https://example.com/$id.png", -) - -/** Minimal detail domain fixture. */ -fun characterDetails(id: Int): CharacterDetails = CharacterDetails( - id = id, - name = "Character $id", - status = CharacterStatus.ALIVE, - species = "Human", - type = "Genetic experiment", - gender = "Male", - origin = "Earth (C-137)", - location = "Citadel of Ricks", - imageUrl = "https://example.com/$id.png", - episodeCount = 10, -) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d2143e4..43e5901 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ junitJupiter = "5.11.4" junitPlatform = "1.11.4" turbine = "1.2.0" assertk = "0.28.1" +mockk = "1.14.3" androidxTestExt = "1.3.0" androidxTestRunner = "1.7.0" androidxEspresso = "3.7.0" @@ -119,6 +120,7 @@ junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", vers junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatform" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" } androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidxEspresso" } @@ -142,7 +144,7 @@ ktor = [ ] lifecycle-compose = ["androidx-lifecycle-runtime-compose", "androidx-lifecycle-viewmodel-compose"] views = ["androidx-appcompat", "material", "androidx-recyclerview", "androidx-fragment-ktx"] -unit-test = ["junit-jupiter-api", "kotlinx-coroutines-test", "turbine", "assertk"] +unit-test = ["junit-jupiter-api", "kotlinx-coroutines-test", "turbine", "assertk", "mockk"] # Instrumented Compose UI test (androidTest): ComposeTestRule + AndroidJUnit4 runner. # espresso-core/runner are pinned to current versions: Compose's test rule drives Espresso's # onIdle, and the transitive espresso 3.5.0 calls InputManager.getInstance() (removed on API 34+),