Skip to content

Commit 86edfb4

Browse files
committed
Tidied up ViewModels and ViewModel tests.
Moved mapping in HomeViewModel to saveConnectionDetails.
1 parent f8a6ab6 commit 86edfb4

4 files changed

Lines changed: 103 additions & 48 deletions

File tree

  • architecture/presentation-test/src/main/java/com/mitteloupe/whoami/architecture/presentation/viewmodel
  • history/presentation/src
  • home/presentation/src/main/java/com/mitteloupe/whoami/home/presentation/viewmodel

architecture/presentation-test/src/main/java/com/mitteloupe/whoami/architecture/presentation/viewmodel/BaseViewModelTest.kt

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.mitteloupe.whoami.architecture.domain.usecase.UseCase
66
import com.mitteloupe.whoami.architecture.presentation.notification.PresentationNotification
77
import kotlinx.coroutines.Dispatchers
88
import kotlinx.coroutines.ExperimentalCoroutinesApi
9-
import kotlinx.coroutines.runBlocking
109
import kotlinx.coroutines.test.TestCoroutineScheduler
1110
import kotlinx.coroutines.test.UnconfinedTestDispatcher
1211
import kotlinx.coroutines.test.resetMain
@@ -17,7 +16,11 @@ import org.mockito.BDDMockito.willAnswer
1716
import org.mockito.Mock
1817
import org.mockito.kotlin.any
1918
import org.mockito.kotlin.eq
19+
import org.mockito.stubbing.Answer
2020

21+
private const val NO_INPUT_ON_RESULT_ARGUMENT_INDEX = 1
22+
private const val NO_INPUT_ON_EXCEPTION_ARGUMENT_INDEX = 2
23+
private const val ON_RESULT_ARGUMENT_INDEX = 2
2124
private const val ON_EXCEPTION_ARGUMENT_INDEX = 3
2225

2326
abstract class BaseViewModelTest<
@@ -47,38 +50,33 @@ abstract class BaseViewModelTest<
4750
Dispatchers.resetMain()
4851
}
4952

53+
protected fun UseCase<Unit, *>.givenFailedExecution(domainException: DomainException) {
54+
givenExecutionWillAnswer { invocation ->
55+
val onException: (DomainException) -> Unit =
56+
invocation.getArgument(NO_INPUT_ON_EXCEPTION_ARGUMENT_INDEX)
57+
onException(domainException)
58+
}
59+
}
60+
5061
protected fun <REQUEST> UseCase<REQUEST, *>.givenFailedExecution(
5162
input: REQUEST,
5263
domainException: DomainException
5364
) {
54-
runBlocking {
55-
willAnswer { invocation ->
56-
val onException: (DomainException) -> Unit =
57-
invocation.getArgument(ON_EXCEPTION_ARGUMENT_INDEX)
58-
onException(domainException)
59-
}.given(useCaseExecutor)
60-
.execute(
61-
useCase = eq(this@givenFailedExecution),
62-
value = eq(input),
63-
onResult = any(),
64-
onException = any()
65-
)
65+
givenExecutionWillAnswer(input) { invocation ->
66+
val onException: (DomainException) -> Unit =
67+
invocation.getArgument(ON_EXCEPTION_ARGUMENT_INDEX)
68+
onException(domainException)
6669
}
6770
}
6871

6972
protected fun <REQUEST, RESULT> UseCase<REQUEST, RESULT>.givenSuccessfulExecution(
7073
input: REQUEST,
7174
result: RESULT
7275
) {
73-
willAnswer { invocationOnMock ->
74-
val onResult: (RESULT) -> Unit = invocationOnMock.getArgument(2)
76+
givenExecutionWillAnswer(input) { invocation ->
77+
val onResult: (RESULT) -> Unit = invocation.getArgument(ON_RESULT_ARGUMENT_INDEX)
7578
onResult(result)
76-
}.given(useCaseExecutor).execute(
77-
useCase = eq(this@givenSuccessfulExecution),
78-
value = eq(input),
79-
onResult = any(),
80-
onException = any()
81-
)
79+
}
8280
}
8381

8482
protected fun <REQUEST> UseCase<REQUEST, Unit>.givenSuccessfulNoResultExecution(
@@ -88,22 +86,35 @@ abstract class BaseViewModelTest<
8886
}
8987

9088
protected fun <RESULT> UseCase<Unit, RESULT>.givenSuccessfulExecution(result: RESULT) {
91-
willAnswer { invocationOnMock ->
92-
val onResult: (RESULT) -> Unit = invocationOnMock.getArgument(1)
89+
givenExecutionWillAnswer { invocationOnMock ->
90+
val onResult: (RESULT) -> Unit =
91+
invocationOnMock.getArgument(NO_INPUT_ON_RESULT_ARGUMENT_INDEX)
9392
onResult(result)
94-
}.given(useCaseExecutor).execute(
95-
useCase = eq(this@givenSuccessfulExecution),
93+
}
94+
}
95+
96+
protected fun UseCase<Unit, Unit>.givenSuccessfulNoArgumentNoResultExecution() {
97+
givenExecutionWillAnswer { invocationOnMock ->
98+
val onResult: (Unit) -> Unit = invocationOnMock.getArgument(ON_RESULT_ARGUMENT_INDEX)
99+
onResult(Unit)
100+
}
101+
}
102+
103+
private fun <RESULT> UseCase<Unit, RESULT>.givenExecutionWillAnswer(answer: Answer<*>) {
104+
willAnswer(answer).given(useCaseExecutor).execute(
105+
useCase = eq(this@givenExecutionWillAnswer),
96106
onResult = any(),
97107
onException = any()
98108
)
99109
}
100110

101-
protected fun UseCase<Unit, Unit>.givenSuccessfulNoArgumentNoResultExecution() {
102-
willAnswer { invocationOnMock ->
103-
val onResult: (Unit) -> Unit = invocationOnMock.getArgument(2)
104-
onResult(Unit)
105-
}.given(useCaseExecutor).execute(
106-
useCase = eq(this@givenSuccessfulNoArgumentNoResultExecution),
111+
private fun <REQUEST, RESULT> UseCase<REQUEST, RESULT>.givenExecutionWillAnswer(
112+
input: REQUEST,
113+
answer: Answer<*>
114+
) {
115+
willAnswer(answer).given(useCaseExecutor).execute(
116+
useCase = eq(this@givenExecutionWillAnswer),
117+
value = eq(input),
107118
onResult = any(),
108119
onException = any()
109120
)

history/presentation/src/main/java/com/mitteloupe/whoami/history/presentation/viewmodel/HistoryViewModel.kt

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.mitteloupe.whoami.architecture.domain.UseCaseExecutor
44
import com.mitteloupe.whoami.architecture.presentation.navigation.PresentationNavigationEvent.Back
55
import com.mitteloupe.whoami.architecture.presentation.notification.PresentationNotification
66
import com.mitteloupe.whoami.architecture.presentation.viewmodel.BaseViewModel
7+
import com.mitteloupe.whoami.history.domain.model.SavedIpAddressRecordDomainModel
78
import com.mitteloupe.whoami.history.domain.usecase.DeleteHistoryRecordUseCase
89
import com.mitteloupe.whoami.history.domain.usecase.GetHistoryUseCase
910
import com.mitteloupe.whoami.history.presentation.mapper.DeleteHistoryRecordRequestDomainMapper
@@ -22,25 +23,51 @@ class HistoryViewModel(
2223
) : BaseViewModel<HistoryViewState, PresentationNotification>(useCaseExecutor) {
2324
fun onEnter(highlightedIpAddress: String?) {
2425
updateViewState(Loading)
26+
fetchRecordHistory(highlightedIpAddress)
27+
}
28+
29+
fun onDeleteAction(deletionRequest: HistoryRecordDeletionPresentationModel) {
30+
deleteHistoryRecord(deletionRequest)
31+
}
32+
33+
fun onBackAction() {
34+
emitNavigationEvent(Back)
35+
}
36+
37+
private fun fetchRecordHistory(highlightedIpAddress: String?) {
2538
getHistoryUseCase(
2639
onResult = { result ->
27-
updateViewState(
28-
HistoryRecords(
29-
highlightedIpAddress = highlightedIpAddress,
30-
historyRecords = result
31-
.map(savedIpAddressRecordPresentationMapper::toPresentation)
32-
)
40+
presentRecordHistory(
41+
highlightedIpAddress = highlightedIpAddress,
42+
domainHistoryRecords = result
3343
)
3444
},
3545
onException = { exception ->
46+
presentRecordHistory(
47+
highlightedIpAddress = null,
48+
domainHistoryRecords = emptySet()
49+
)
3650
println("UNEXPECTED: $exception")
3751
}
3852
)
3953
}
4054

41-
fun onDeleteAction(deletionRequest: HistoryRecordDeletionPresentationModel) {
42-
val domainDeletionRequest =
43-
deleteHistoryRecordRequestDomainMapper.toDomain(deletionRequest)
55+
private fun presentRecordHistory(
56+
highlightedIpAddress: String?,
57+
domainHistoryRecords: Collection<SavedIpAddressRecordDomainModel>
58+
) {
59+
val presentationHistoryRecords = domainHistoryRecords
60+
.map(savedIpAddressRecordPresentationMapper::toPresentation)
61+
updateViewState(
62+
HistoryRecords(
63+
highlightedIpAddress = highlightedIpAddress,
64+
historyRecords = presentationHistoryRecords
65+
)
66+
)
67+
}
68+
69+
private fun deleteHistoryRecord(deletionRequest: HistoryRecordDeletionPresentationModel) {
70+
val domainDeletionRequest = deleteHistoryRecordRequestDomainMapper.toDomain(deletionRequest)
4471
deleteHistoryRecordUseCase(
4572
value = domainDeletionRequest,
4673
onResult = {},
@@ -49,8 +76,4 @@ class HistoryViewModel(
4976
}
5077
)
5178
}
52-
53-
fun onBackAction() {
54-
emitNavigationEvent(Back)
55-
}
5679
}

history/presentation/src/test/java/com/mitteloupe/whoami/history/presentation/viewmodel/HistoryViewModelTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mitteloupe.whoami.history.presentation.viewmodel
22

3+
import com.mitteloupe.whoami.architecture.domain.exception.UnknownDomainException
34
import com.mitteloupe.whoami.architecture.presentation.notification.PresentationNotification
45
import com.mitteloupe.whoami.architecture.presentation.viewmodel.BaseViewModelTest
56
import com.mitteloupe.whoami.history.domain.model.HistoryRecordDeletionDomainModel
@@ -20,7 +21,9 @@ import kotlinx.coroutines.flow.take
2021
import kotlinx.coroutines.flow.toList
2122
import kotlinx.coroutines.runBlocking
2223
import kotlinx.coroutines.test.runTest
24+
import org.hamcrest.CoreMatchers.`is`
2325
import org.hamcrest.MatcherAssert.assertThat
26+
import org.hamcrest.Matchers.empty
2427
import org.hamcrest.collection.IsCollectionWithSize.hasSize
2528
import org.hamcrest.core.IsIterableContaining.hasItem
2629
import org.junit.Assert.assertEquals
@@ -126,6 +129,24 @@ class HistoryViewModelTest :
126129
)
127130
}
128131

132+
@Test
133+
fun `Given history error when onEnter then presents empty history`() = runTest {
134+
// Given
135+
getHistoryUseCase.givenFailedExecution(UnknownDomainException())
136+
val highlightedIpAddress = "0.0.0.0"
137+
val deferredViewState = async(start = UNDISPATCHED) {
138+
classUnderTest.viewState.take(2).toList()
139+
}
140+
141+
// When
142+
classUnderTest.onEnter(highlightedIpAddress)
143+
val actualValue = deferredViewState.await()
144+
145+
// Then
146+
assertEquals(Loading, actualValue[0])
147+
assertThat((actualValue[1] as HistoryRecords).historyRecords, `is`(empty()))
148+
}
149+
129150
@Test
130151
fun `Given deletion request when onDeleteAction then deletes record`() {
131152
// Given

home/presentation/src/main/java/com/mitteloupe/whoami/home/presentation/viewmodel/HomeViewModel.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ class HomeViewModel(
3232
}
3333

3434
fun onSaveDetailsAction(connectionDetails: HomeViewState.Connected) {
35-
val domainConnectionDetails = connectionDetailsDomainMapper.toDomain(connectionDetails)
36-
saveConnectionDetails(domainConnectionDetails)
35+
saveConnectionDetails(connectionDetails)
3736
}
3837

3938
fun onViewHistoryAction() {
@@ -55,9 +54,10 @@ class HomeViewModel(
5554
updateViewState(connectionDetailsPresentationMapper.toPresentation(connectionDetails))
5655
}
5756

58-
private fun saveConnectionDetails(connectionDetails: ConnectionDetailsDomainModel.Connected) {
57+
private fun saveConnectionDetails(connectionDetails: HomeViewState.Connected) {
58+
val domainConnectionDetails = connectionDetailsDomainMapper.toDomain(connectionDetails)
5959
saveConnectionDetailsUseCase(
60-
value = connectionDetails,
60+
value = domainConnectionDetails,
6161
onResult = { presentSaveDetailsResult(connectionDetails.ipAddress) },
6262
onException = ::presentError
6363
)

0 commit comments

Comments
 (0)