Skip to content

Commit 1404be1

Browse files
TDD4: add PropertyListViewModelFlow.kt tests and implementation
Note: tests returning result are flaky, find the cause
1 parent 7d6fb79 commit 1404be1

6 files changed

Lines changed: 309 additions & 16 deletions

File tree

app/src/main/java/com/smarttoolfactory/propertyfindar/ui/BottomNavigationFragmentStateAdapter.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,26 @@ class BottomNavigationFragmentStateAdapter(fragmentManager: FragmentManager, lif
2424

2525
override fun createFragment(position: Int): Fragment {
2626
return when (position) {
27+
28+
// Home Dynamic Feature Module
2729
0 -> NavHostContainerFragment.createNavHostContainerFragment(
2830
R.layout.fragment_navhost_home,
2931
R.id.nested_nav_host_fragment_home
3032
)
3133

32-
// Vertical NavHost Post Fragment Container
34+
// Favorites Dynamic Feature Module
3335
1 -> NavHostContainerFragment.createNavHostContainerFragment(
3436
R.layout.fragment_navhost_favorites,
3537
R.id.nested_nav_host_fragment_favorites
3638
)
3739

38-
// Horizontal NavHost Post Fragment Container
40+
// Notification Dynamic Feature Module
3941
2 -> NavHostContainerFragment.createNavHostContainerFragment(
4042
R.layout.fragment_navhost_notification,
4143
R.id.nested_nav_host_fragment_notification
4244
)
4345

46+
// Notification Account Feature Module
4447
else -> NavHostContainerFragment.createNavHostContainerFragment(
4548
R.layout.fragment_navhost_account,
4649
R.id.nested_nav_host_fragment_account
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.smarttoolfactory.home.viewmodel
2+
3+
import androidx.hilt.lifecycle.ViewModelInject
4+
import androidx.lifecycle.LiveData
5+
import androidx.lifecycle.MutableLiveData
6+
import com.smarttoolfactory.core.util.Event
7+
import com.smarttoolfactory.core.util.convertToFlowViewState
8+
import com.smarttoolfactory.core.viewstate.Status
9+
import com.smarttoolfactory.core.viewstate.ViewState
10+
import com.smarttoolfactory.domain.model.PropertyItem
11+
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
12+
import kotlinx.coroutines.CoroutineScope
13+
import kotlinx.coroutines.flow.launchIn
14+
import kotlinx.coroutines.flow.onEach
15+
import kotlinx.coroutines.flow.onStart
16+
17+
class PropertyListViewModelFlow @ViewModelInject constructor(
18+
private val coroutineScope: CoroutineScope,
19+
private val getPropertiesUseCase: GetPropertiesUseCaseFlow
20+
) : AbstractPropertyListVM() {
21+
22+
private val _goToDetailScreen = MutableLiveData<Event<PropertyItem>>()
23+
24+
override val goToDetailScreen: LiveData<Event<PropertyItem>>
25+
get() = _goToDetailScreen
26+
27+
private val _propertyViewState = MutableLiveData<ViewState<List<PropertyItem>>>()
28+
29+
override val propertyListViewState: LiveData<ViewState<List<PropertyItem>>>
30+
get() = _propertyViewState
31+
32+
/**
33+
* Function to retrieve data from repository with offline-first which checks
34+
* local data source first.
35+
*
36+
* * Check out Local Source first
37+
* * If empty data or null returned throw empty set exception
38+
* * If error occurred while fetching data from remote: Try to fetch data from db
39+
* * If data is fetched from remote source: delete old data, save new data and return new data
40+
* * If both network and db don't have any data throw empty set exception
41+
*
42+
*/
43+
override fun getPropertyList() {
44+
45+
getPropertiesUseCase.getPropertiesOfflineFirst("")
46+
.convertToFlowViewState()
47+
.onStart {
48+
_propertyViewState.value = ViewState(status = Status.LOADING)
49+
}
50+
.onEach {
51+
_propertyViewState.value = it
52+
}
53+
.launchIn(coroutineScope)
54+
}
55+
56+
override fun refreshPropertyList() {
57+
getPropertiesUseCase.getPropertiesOfflineLast("")
58+
.convertToFlowViewState()
59+
.onStart {
60+
_propertyViewState.value = ViewState(status = Status.LOADING)
61+
}
62+
.onEach {
63+
_propertyViewState.value = it
64+
}
65+
.launchIn(coroutineScope)
66+
}
67+
68+
override fun onClick(item: PropertyItem) {
69+
_goToDetailScreen.value = Event(item)
70+
}
71+
}

features/home/src/main/java/com/smarttoolfactory/home/viewmodel/ViewModelFactory.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,29 @@ package com.smarttoolfactory.home.viewmodel
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.ViewModelProvider
5+
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
56
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseRxJava3
67
import javax.inject.Inject
8+
import kotlinx.coroutines.CoroutineScope
79

8-
class ViewModelFactory
10+
class PropertyListFlowViewModelFactory @Inject constructor(
11+
private val coroutineScope: CoroutineScope,
12+
private val getPropertiesUseCase: GetPropertiesUseCaseFlow
13+
) : ViewModelProvider.Factory {
14+
15+
@Suppress("UNCHECKED_CAST")
16+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
17+
18+
if (modelClass != PropertyListViewModelFlow::class.java) {
19+
throw IllegalArgumentException("Unknown ViewModel class")
20+
}
21+
22+
return PropertyListViewModelFlow(
23+
coroutineScope,
24+
getPropertiesUseCase
25+
) as T
26+
}
27+
}
928

1029
class PropertyListRxJava3ViewModelFactory @Inject constructor(
1130
private val getPropertiesUseCase: GetPropertiesUseCaseRxJava3
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package com.smarttoolfactory.home.viewmodel
2+
3+
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4+
import com.google.common.truth.Truth
5+
import com.smarttoolfactory.core.viewstate.Status
6+
import com.smarttoolfactory.domain.model.PropertyItem
7+
import com.smarttoolfactory.domain.usecase.GetPropertiesUseCaseFlow
8+
import com.smarttoolfactory.test_utils.RESPONSE_JSON_PATH
9+
import com.smarttoolfactory.test_utils.rule.TestCoroutineRule
10+
import com.smarttoolfactory.test_utils.test_observer.test
11+
import com.smarttoolfactory.test_utils.util.convertToObjectFromJson
12+
import com.smarttoolfactory.test_utils.util.getResourceAsText
13+
import io.mockk.clearMocks
14+
import io.mockk.every
15+
import io.mockk.mockk
16+
import io.mockk.verify
17+
import kotlinx.coroutines.flow.flow
18+
import org.junit.After
19+
import org.junit.Before
20+
import org.junit.Rule
21+
import org.junit.Test
22+
23+
class PropertyListViewModelFlowTest {
24+
25+
// Run tasks synchronously
26+
/**
27+
* Not using this causes java.lang.RuntimeException: Method getMainLooper in android.os.Looper
28+
* not mocked when `this.observeForever(observer)` is called
29+
*/
30+
@Rule
31+
@JvmField
32+
val instantExecutorRule = InstantTaskExecutorRule()
33+
34+
/**
35+
* Rule for testing Coroutines with [TestCoroutineScope] and [TestCoroutineDispatcher]
36+
*/
37+
@Rule
38+
@JvmField
39+
var testCoroutineRule = TestCoroutineRule()
40+
41+
companion object {
42+
43+
private data class PropertyItems(
44+
val total: Int,
45+
val res: List<PropertyItem>
46+
)
47+
48+
private val itemList =
49+
convertToObjectFromJson<PropertyItems>(
50+
getResourceAsText(RESPONSE_JSON_PATH)
51+
)!!.res
52+
}
53+
54+
/*
55+
Mocks
56+
*/
57+
private val useCase: GetPropertiesUseCaseFlow = mockk()
58+
59+
/**
60+
* ViewModel to test list which is SUT
61+
*/
62+
private lateinit var viewModel: PropertyListViewModelFlow
63+
64+
@Test
65+
fun `given exception returned from useCase, should have ViewState ERROR offlineFirst`() =
66+
testCoroutineRule.runBlockingTest {
67+
68+
// GIVEN
69+
every {
70+
useCase.getPropertiesOfflineFirst()
71+
} returns flow<List<PropertyItem>> {
72+
emit(throw Exception("Network Exception"))
73+
}
74+
75+
val testObserver = viewModel.propertyListViewState.test()
76+
77+
// WHEN
78+
viewModel.getPropertyList()
79+
80+
// THEN
81+
testObserver
82+
.assertValue { states ->
83+
(
84+
states[0].status == Status.LOADING &&
85+
states[1].status == Status.ERROR
86+
)
87+
}
88+
89+
val finalState = testObserver.values()[1]
90+
Truth.assertThat(finalState.error?.message).isEqualTo("Network Exception")
91+
Truth.assertThat(finalState.error).isInstanceOf(Exception::class.java)
92+
verify(atMost = 1) { useCase.getPropertiesOfflineFirst() }
93+
}
94+
95+
/**
96+
* ❌ FIXME This test is flaky, find out the cause, sometimes null is returned
97+
*/
98+
@Test
99+
fun `given useCase fetched data, should have ViewState SUCCESS and data offlineFirst`() =
100+
testCoroutineRule.runBlockingTest {
101+
102+
// GIVEN
103+
every { useCase.getPropertiesOfflineFirst() } returns flow {
104+
emit(itemList)
105+
}
106+
107+
val testObserver = viewModel.propertyListViewState.test()
108+
109+
// WHEN
110+
viewModel.getPropertyList()
111+
112+
advanceUntilIdle()
113+
114+
// THEN
115+
val viewStates = testObserver.values()
116+
Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING)
117+
118+
val actual = viewStates.last().data
119+
120+
Truth.assertThat(actual?.size).isEqualTo(itemList.size)
121+
verify(exactly = 1) { useCase.getPropertiesOfflineFirst() }
122+
testObserver.dispose()
123+
}
124+
125+
@Test
126+
fun `given exception returned from useCase, should have ViewState ERROR offlineLast`() =
127+
testCoroutineRule.runBlockingTest {
128+
129+
// GIVEN
130+
every {
131+
useCase.getPropertiesOfflineLast()
132+
} returns flow<List<PropertyItem>> {
133+
emit(throw Exception("Network Exception"))
134+
}
135+
136+
val testObserver = viewModel.propertyListViewState.test()
137+
138+
// WHEN
139+
viewModel.refreshPropertyList()
140+
141+
// THEN
142+
testObserver
143+
.assertValue { states ->
144+
(
145+
states[0].status == Status.LOADING &&
146+
states[1].status == Status.ERROR
147+
)
148+
}
149+
.dispose()
150+
151+
val finalState = testObserver.values()[1]
152+
Truth.assertThat(finalState.error?.message).isEqualTo("Network Exception")
153+
Truth.assertThat(finalState.error).isInstanceOf(Exception::class.java)
154+
verify(atMost = 1) { useCase.getPropertiesOfflineLast() }
155+
}
156+
157+
/**
158+
* ❌ FIXME This test is flaky, find out the cause, sometimes null is returned
159+
*/
160+
@Test
161+
fun `given useCase fetched data, should have ViewState SUCCESS and data offlineLast`() =
162+
testCoroutineRule.runBlockingTest {
163+
164+
// GIVEN
165+
every {
166+
useCase.getPropertiesOfflineLast()
167+
} returns flow {
168+
emit(itemList)
169+
}
170+
171+
val testObserver = viewModel.propertyListViewState.test()
172+
173+
// WHEN
174+
viewModel.refreshPropertyList()
175+
176+
// THEN
177+
val viewStates = testObserver.values()
178+
Truth.assertThat(viewStates.first().status).isEqualTo(Status.LOADING)
179+
180+
val actual = viewStates.last().data
181+
Truth.assertThat(actual?.size).isEqualTo(itemList.size)
182+
verify(exactly = 1) { useCase.getPropertiesOfflineLast() }
183+
testObserver.dispose()
184+
}
185+
186+
@Before
187+
fun setUp() {
188+
viewModel =
189+
PropertyListViewModelFlow(testCoroutineRule.testCoroutineScope, useCase)
190+
}
191+
192+
@After
193+
fun tearDown() {
194+
clearMocks(useCase)
195+
}
196+
}

features/home/src/test/java/com/smarttoolfactory/home/viewmodel/PropertyListViewModelRxJava3Test.kt

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,28 @@ class PropertyListViewModelRxJava3Test {
4141
@JvmField
4242
val rxImmediateSchedulerRule = RxImmediateSchedulerRule()
4343

44-
private val itemList = convertToObjectFromJson<PopertItems>(
45-
getResourceAsText(RESPONSE_JSON_PATH)
46-
)!!.res
44+
companion object {
45+
46+
private data class PropertyItems(
47+
val total: Int,
48+
val res: List<PropertyItem>
49+
)
50+
51+
private val itemList = convertToObjectFromJson<PropertyItems>(
52+
getResourceAsText(RESPONSE_JSON_PATH)
53+
)!!.res
54+
}
4755

4856
/*
4957
Mocks
5058
*/
5159
private val useCase: GetPropertiesUseCaseRxJava3 = mockk()
5260

5361
/**
54-
* ViewModel to test post list which is SUT
62+
* ViewModel to test list which is SUT
5563
*/
5664
private lateinit var viewModel: PropertyListViewModelRxJava3
5765

58-
data class PopertItems(
59-
val total: Int,
60-
val res: List<PropertyItem>
61-
)
62-
6366
@Test
6467
fun `given exception returned from useCase, should have ViewState ERROR offlineFirst`() {
6568

0 commit comments

Comments
 (0)