Skip to content

Commit a87c5ef

Browse files
feat: junit tests
1 parent 5b0a9f9 commit a87c5ef

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
package org.openedx.dates
2+
3+
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4+
import androidx.fragment.app.FragmentManager
5+
import io.mockk.coEvery
6+
import io.mockk.coVerify
7+
import io.mockk.every
8+
import io.mockk.mockk
9+
import io.mockk.verify
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.ExperimentalCoroutinesApi
12+
import kotlinx.coroutines.async
13+
import kotlinx.coroutines.flow.first
14+
import kotlinx.coroutines.test.StandardTestDispatcher
15+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
16+
import kotlinx.coroutines.test.advanceUntilIdle
17+
import kotlinx.coroutines.test.resetMain
18+
import kotlinx.coroutines.test.runTest
19+
import kotlinx.coroutines.test.setMain
20+
import kotlinx.coroutines.withTimeoutOrNull
21+
import org.junit.After
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Assert.assertFalse
24+
import org.junit.Assert.assertTrue
25+
import org.junit.Before
26+
import org.junit.Rule
27+
import org.junit.Test
28+
import org.openedx.core.R
29+
import org.openedx.core.data.storage.CorePreferences
30+
import org.openedx.core.domain.model.CourseDate
31+
import org.openedx.core.domain.model.CourseDatesResponse
32+
import org.openedx.core.system.connection.NetworkConnection
33+
import org.openedx.dates.domain.interactor.DatesInteractor
34+
import org.openedx.dates.presentation.DatesRouter
35+
import org.openedx.dates.presentation.dates.DatesViewModel
36+
import org.openedx.foundation.presentation.UIMessage
37+
import org.openedx.foundation.system.ResourceManager
38+
import java.net.UnknownHostException
39+
import java.util.Date
40+
41+
@OptIn(ExperimentalCoroutinesApi::class)
42+
class DatesViewModelTest {
43+
44+
@get:Rule
45+
val testInstantTaskExecutorRule = InstantTaskExecutorRule()
46+
47+
private val dispatcher = StandardTestDispatcher()
48+
49+
private val datesRouter = mockk<DatesRouter>(relaxed = true)
50+
private val networkConnection = mockk<NetworkConnection>()
51+
private val resourceManager = mockk<ResourceManager>()
52+
private val datesInteractor = mockk<DatesInteractor>()
53+
private val corePreferences = mockk<CorePreferences>()
54+
55+
private val noInternet = "Slow or no internet connection"
56+
private val somethingWrong = "Something went wrong"
57+
58+
@Before
59+
fun setUp() {
60+
Dispatchers.setMain(dispatcher)
61+
every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet
62+
every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong
63+
// By default, assume we have an internet connection
64+
every { networkConnection.isOnline() } returns true
65+
every { corePreferences.isRelativeDatesEnabled } returns true
66+
}
67+
68+
@After
69+
fun tearDown() {
70+
Dispatchers.resetMain()
71+
}
72+
73+
@Test
74+
fun `init fetchDates online with pagination`() = runTest {
75+
// Create a dummy CourseDate; grouping is done inside the view model so the exact grouping is not under test.
76+
val courseDate: CourseDate = mockk(relaxed = true)
77+
val courseDatesResponse = CourseDatesResponse(
78+
count = 10,
79+
next = 2,
80+
previous = 1,
81+
results = listOf(courseDate)
82+
)
83+
coEvery { datesInteractor.getUserDates(1) } returns courseDatesResponse
84+
85+
// Instantiate the view model; fetchDates is called in init.
86+
val viewModel = DatesViewModel(
87+
datesRouter,
88+
networkConnection,
89+
resourceManager,
90+
datesInteractor,
91+
corePreferences
92+
)
93+
advanceUntilIdle()
94+
95+
coVerify(exactly = 1) { datesInteractor.getUserDates(1) }
96+
// Since next is not null and page (1) != count (10), canLoadMore should be true.
97+
assertFalse(viewModel.uiState.value.isLoading)
98+
assertTrue(viewModel.uiState.value.canLoadMore)
99+
}
100+
101+
@Test
102+
fun `init fetchDates offline uses cache`() = runTest {
103+
every { networkConnection.isOnline() } returns false
104+
val cachedCourseDate: CourseDate = mockk(relaxed = true)
105+
coEvery { datesInteractor.getUserDatesFromCache() } returns listOf(cachedCourseDate)
106+
107+
val viewModel = DatesViewModel(
108+
datesRouter,
109+
networkConnection,
110+
resourceManager,
111+
datesInteractor,
112+
corePreferences
113+
)
114+
advanceUntilIdle()
115+
116+
// When offline, getUserDates is not called.
117+
coVerify(exactly = 0) { datesInteractor.getUserDates(any()) }
118+
coVerify(exactly = 1) { datesInteractor.getUserDatesFromCache() }
119+
assertFalse(viewModel.uiState.value.isLoading)
120+
// Expect no further pages to load.
121+
assertFalse(viewModel.uiState.value.canLoadMore)
122+
}
123+
124+
@Test
125+
fun `fetchDates unknown error emits unknown error message`() =
126+
runTest(UnconfinedTestDispatcher()) {
127+
every { networkConnection.isOnline() } returns true
128+
129+
val viewModel = DatesViewModel(
130+
datesRouter,
131+
networkConnection,
132+
resourceManager,
133+
datesInteractor,
134+
corePreferences
135+
)
136+
val message = async {
137+
withTimeoutOrNull(5000) {
138+
viewModel.uiMessage.first() as? UIMessage.SnackBarMessage
139+
}
140+
}
141+
advanceUntilIdle()
142+
143+
assertEquals(somethingWrong, message.await()?.message)
144+
assertFalse(viewModel.uiState.value.isLoading)
145+
}
146+
147+
@Test
148+
fun `fetchDates internet error emits no connection message`() =
149+
runTest(UnconfinedTestDispatcher()) {
150+
every { networkConnection.isOnline() } returns true
151+
coEvery { datesInteractor.getUserDates(any()) } throws UnknownHostException()
152+
153+
val viewModel = DatesViewModel(
154+
datesRouter,
155+
networkConnection,
156+
resourceManager,
157+
datesInteractor,
158+
corePreferences
159+
)
160+
val message = async {
161+
withTimeoutOrNull(5000) {
162+
viewModel.uiMessage.first() as? UIMessage.SnackBarMessage
163+
}
164+
}
165+
advanceUntilIdle()
166+
167+
assertEquals(noInternet, message.await()?.message)
168+
assertFalse(viewModel.uiState.value.isLoading)
169+
}
170+
171+
@Test
172+
fun `shiftDueDate success`() = runTest {
173+
every { networkConnection.isOnline() } returns true
174+
// Prepare a dummy CourseDate that qualifies as past due and is marked as relative.
175+
val courseDate: CourseDate = mockk(relaxed = true) {
176+
every { relative } returns true
177+
every { courseId } returns "course-123"
178+
// Set dueDate to yesterday.
179+
every { dueDate } returns Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000)
180+
}
181+
val courseDatesResponse = CourseDatesResponse(
182+
count = 1,
183+
next = null,
184+
previous = null,
185+
results = listOf(courseDate)
186+
)
187+
coEvery { datesInteractor.getUserDates(1) } returns courseDatesResponse
188+
// When refreshData is triggered from shiftDueDate, return the same response.
189+
coEvery { datesInteractor.getUserDates(any()) } returns courseDatesResponse
190+
191+
val viewModel = DatesViewModel(
192+
datesRouter,
193+
networkConnection,
194+
resourceManager,
195+
datesInteractor,
196+
corePreferences
197+
)
198+
advanceUntilIdle()
199+
200+
viewModel.shiftDueDate()
201+
advanceUntilIdle()
202+
203+
coVerify { datesInteractor.shiftDueDate(listOf("course-123")) }
204+
// isShiftDueDatesPressed should be reset to false after processing.
205+
assertFalse(viewModel.uiState.value.isShiftDueDatesPressed)
206+
}
207+
208+
@Test
209+
fun `shiftDueDate error emits error message and resets flag`() =
210+
runTest(UnconfinedTestDispatcher()) {
211+
every { networkConnection.isOnline() } returns true
212+
val courseDate: CourseDate = mockk(relaxed = true) {
213+
every { relative } returns true
214+
every { courseId } returns "course-123"
215+
every { dueDate } returns Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000)
216+
}
217+
val courseDatesResponse = CourseDatesResponse(
218+
count = 1,
219+
next = null,
220+
previous = null,
221+
results = listOf(courseDate)
222+
)
223+
coEvery { datesInteractor.getUserDates(1) } returns courseDatesResponse
224+
coEvery { datesInteractor.shiftDueDate(any()) } throws Exception()
225+
226+
val viewModel = DatesViewModel(
227+
datesRouter,
228+
networkConnection,
229+
resourceManager,
230+
datesInteractor,
231+
corePreferences
232+
)
233+
advanceUntilIdle()
234+
235+
viewModel.shiftDueDate()
236+
val message = async {
237+
withTimeoutOrNull(5000) {
238+
viewModel.uiMessage.first() as? UIMessage.SnackBarMessage
239+
}
240+
}
241+
advanceUntilIdle()
242+
243+
assertEquals(somethingWrong, message.await()?.message)
244+
assertFalse(viewModel.uiState.value.isShiftDueDatesPressed)
245+
}
246+
247+
@Test
248+
fun `onSettingsClick navigates to settings`() = runTest {
249+
val viewModel = DatesViewModel(
250+
datesRouter,
251+
networkConnection,
252+
resourceManager,
253+
datesInteractor,
254+
corePreferences
255+
)
256+
val fragmentManager = mockk<FragmentManager>(relaxed = true)
257+
258+
viewModel.onSettingsClick(fragmentManager)
259+
verify { datesRouter.navigateToSettings(fragmentManager) }
260+
}
261+
262+
@Test
263+
fun `navigateToCourseOutline calls router with correct parameters`() = runTest {
264+
val viewModel = DatesViewModel(
265+
datesRouter,
266+
networkConnection,
267+
resourceManager,
268+
datesInteractor,
269+
corePreferences
270+
)
271+
val fragmentManager = mockk<FragmentManager>(relaxed = true)
272+
val courseDate: CourseDate = mockk(relaxed = true) {
273+
every { courseId } returns "course-123"
274+
every { courseName } returns "Test Course"
275+
every { assignmentBlockId } returns "block-1"
276+
}
277+
278+
viewModel.navigateToCourseOutline(fragmentManager, courseDate)
279+
verify {
280+
datesRouter.navigateToCourseOutline(
281+
fm = fragmentManager,
282+
courseId = "course-123",
283+
courseTitle = "Test Course",
284+
openTab = "",
285+
resumeBlockId = "block-1"
286+
)
287+
}
288+
}
289+
290+
@Test
291+
fun `fetchMore calls fetchDates when allowed`() = runTest {
292+
every { networkConnection.isOnline() } returns true
293+
val courseDate: CourseDate = mockk(relaxed = true)
294+
val courseDatesResponse = CourseDatesResponse(
295+
count = 10,
296+
next = 2,
297+
previous = 1,
298+
results = listOf(courseDate)
299+
)
300+
301+
// Initial fetch on page 1.
302+
coEvery { datesInteractor.getUserDates(1) } returns courseDatesResponse
303+
// For subsequent fetch, we return a similar response.
304+
coEvery { datesInteractor.getUserDates(any()) } returns courseDatesResponse
305+
306+
val viewModel = DatesViewModel(
307+
datesRouter,
308+
networkConnection,
309+
resourceManager,
310+
datesInteractor,
311+
corePreferences
312+
)
313+
advanceUntilIdle()
314+
315+
viewModel.fetchMore()
316+
advanceUntilIdle()
317+
318+
// Expect two calls (one from init and one from fetchMore)
319+
coVerify(exactly = 2) { datesInteractor.getUserDates(any()) }
320+
}
321+
322+
@Test
323+
fun `refreshData calls fetchDates with refresh true`() = runTest {
324+
every { networkConnection.isOnline() } returns true
325+
val courseDate: CourseDate = mockk(relaxed = true)
326+
val courseDatesResponse = CourseDatesResponse(
327+
count = 1,
328+
next = null,
329+
previous = null,
330+
results = listOf(courseDate)
331+
)
332+
// Initial fetch.
333+
coEvery { datesInteractor.getUserDates(1) } returns courseDatesResponse
334+
// For refresh, return the same response.
335+
coEvery { datesInteractor.getUserDates(any()) } returns courseDatesResponse
336+
337+
val viewModel = DatesViewModel(
338+
datesRouter,
339+
networkConnection,
340+
resourceManager,
341+
datesInteractor,
342+
corePreferences
343+
)
344+
advanceUntilIdle()
345+
346+
viewModel.refreshData()
347+
advanceUntilIdle()
348+
349+
// Two calls: one on init, one on refresh.
350+
coVerify(exactly = 2) { datesInteractor.getUserDates(any()) }
351+
// After refresh, isRefreshing should be false.
352+
assertFalse(viewModel.uiState.value.isRefreshing)
353+
}
354+
}

0 commit comments

Comments
 (0)