Skip to content

Commit c2aac4c

Browse files
committed
refactor(tests): improve dispatcher usage and reduce Compose UI test boilerplate
1 parent 662ceb2 commit c2aac4c

17 files changed

Lines changed: 321 additions & 339 deletions

dynaquiz/composeApp/src/commonTest/kotlin/com/leanite/dynaquiz/core/data/repository/QuizRepositoryImplTest.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import dev.mokkery.matcher.any
1414
import dev.mokkery.mock
1515
import dev.mokkery.verifySuspend
1616
import kotlinx.coroutines.ExperimentalCoroutinesApi
17-
import kotlinx.coroutines.test.UnconfinedTestDispatcher
17+
import kotlinx.coroutines.test.StandardTestDispatcher
1818
import kotlinx.coroutines.test.runTest
1919
import kotlinx.io.IOException
2020
import kotlin.test.Test
@@ -23,7 +23,7 @@ import kotlin.test.assertTrue
2323

2424
@OptIn(ExperimentalCoroutinesApi::class)
2525
class QuizRepositoryImplTest {
26-
private val testDispatcher = UnconfinedTestDispatcher()
26+
private val testDispatcher = StandardTestDispatcher()
2727
private val remoteDataSource = mock<QuizRemoteDataSource>(MockMode.autofill)
2828

2929
private fun createRepository() =
@@ -34,7 +34,7 @@ class QuizRepositoryImplTest {
3434

3535
@Test
3636
fun `getRandomQuestion should return Success with mapped Question when remote responds`() =
37-
runTest {
37+
runTest(testDispatcher) {
3838
everySuspend { remoteDataSource.fetchRandomQuestion() } returns
3939
QuestionDTO(
4040
id = "q-42",
@@ -52,7 +52,7 @@ class QuizRepositoryImplTest {
5252

5353
@Test
5454
fun `getRandomQuestion should return Error NoInternet when remote throws IOException`() =
55-
runTest {
55+
runTest(testDispatcher) {
5656
everySuspend { remoteDataSource.fetchRandomQuestion() } throws IOException("offline")
5757

5858
val result = createRepository().getRandomQuestion()
@@ -63,7 +63,7 @@ class QuizRepositoryImplTest {
6363

6464
@Test
6565
fun `getRandomQuestion should return Error Unknown when remote throws unmapped Throwable`() =
66-
runTest {
66+
runTest(testDispatcher) {
6767
everySuspend { remoteDataSource.fetchRandomQuestion() } throws RuntimeException("boom")
6868

6969
val result = createRepository().getRandomQuestion()
@@ -74,7 +74,7 @@ class QuizRepositoryImplTest {
7474

7575
@Test
7676
fun `submitAnswer should forward unwrapped questionId value and answer to remote`() =
77-
runTest {
77+
runTest(testDispatcher) {
7878
everySuspend { remoteDataSource.submitAnswer(any(), any()) } returns AnswerResultDTO(result = true)
7979

8080
createRepository().submitAnswer(QuestionId("q-7"), answer = "B")
@@ -84,7 +84,7 @@ class QuizRepositoryImplTest {
8484

8585
@Test
8686
fun `submitAnswer should return Success with mapped Answer when remote responds`() =
87-
runTest {
87+
runTest(testDispatcher) {
8888
everySuspend { remoteDataSource.submitAnswer(any(), any()) } returns AnswerResultDTO(result = true)
8989

9090
val result = createRepository().submitAnswer(QuestionId("q-1"), "A")
@@ -95,7 +95,7 @@ class QuizRepositoryImplTest {
9595

9696
@Test
9797
fun `submitAnswer should return Error NoInternet when remote throws IOException`() =
98-
runTest {
98+
runTest(testDispatcher) {
9999
everySuspend { remoteDataSource.submitAnswer(any(), any()) } throws IOException("offline")
100100

101101
val result = createRepository().submitAnswer(QuestionId("q-1"), "A")
@@ -106,15 +106,15 @@ class QuizRepositoryImplTest {
106106

107107
@Test
108108
fun `warmupServer should swallow any Throwable without propagating`() =
109-
runTest {
109+
runTest(testDispatcher) {
110110
everySuspend { remoteDataSource.fetchRandomQuestion() } throws RuntimeException("boom")
111111

112112
createRepository().warmupServer()
113113
}
114114

115115
@Test
116116
fun `warmupServer should call fetchRandomQuestion exactly once`() =
117-
runTest {
117+
runTest(testDispatcher) {
118118
everySuspend { remoteDataSource.fetchRandomQuestion() } returns
119119
QuestionDTO(
120120
id = "q-1",

dynaquiz/composeApp/src/commonTest/kotlin/com/leanite/dynaquiz/core/data/repository/RankingRepositoryImplTest.kt

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import dev.mokkery.matcher.any
1818
import dev.mokkery.mock
1919
import dev.mokkery.verify
2020
import kotlinx.coroutines.ExperimentalCoroutinesApi
21-
import kotlinx.coroutines.test.UnconfinedTestDispatcher
21+
import kotlinx.coroutines.test.StandardTestDispatcher
2222
import kotlinx.coroutines.test.runTest
2323
import kotlinx.io.IOException
2424
import kotlin.test.Test
@@ -29,7 +29,7 @@ import kotlin.time.Instant
2929

3030
@OptIn(ExperimentalCoroutinesApi::class)
3131
class RankingRepositoryImplTest {
32-
private val testDispatcher = UnconfinedTestDispatcher()
32+
private val testDispatcher = StandardTestDispatcher()
3333
private val quizSessionDataSource = mock<QuizSessionLocalDataSource>(MockMode.autofill)
3434
private val playerDataSource = mock<PlayerLocalDataSource>(MockMode.autofill)
3535
private val fixedNowMillis = 1_700_000_000_000L
@@ -63,7 +63,7 @@ class RankingRepositoryImplTest {
6363

6464
@Test
6565
fun `saveSession should ensure player via findOrInsert before inserting the session`() =
66-
runTest {
66+
runTest(testDispatcher) {
6767
every { playerDataSource.findOrInsert(any(), any()) } returns
6868
SelectRankingFixtures.playerEntity(id = 1L, name = "Leandro")
6969

@@ -74,7 +74,7 @@ class RankingRepositoryImplTest {
7474

7575
@Test
7676
fun `saveSession should persist session fields using the playerId returned by findOrInsert`() =
77-
runTest {
77+
runTest(testDispatcher) {
7878
every { playerDataSource.findOrInsert(any(), any()) } returns
7979
SelectRankingFixtures.playerEntity(id = 42L, name = "Leandro")
8080

@@ -94,7 +94,7 @@ class RankingRepositoryImplTest {
9494

9595
@Test
9696
fun `saveSession should return Success Unit on happy path`() =
97-
runTest {
97+
runTest(testDispatcher) {
9898
every { playerDataSource.findOrInsert(any(), any()) } returns
9999
SelectRankingFixtures.playerEntity()
100100

@@ -105,7 +105,7 @@ class RankingRepositoryImplTest {
105105

106106
@Test
107107
fun `saveSession should return Error mapped from Throwable when findOrInsert throws`() =
108-
runTest {
108+
runTest(testDispatcher) {
109109
every { playerDataSource.findOrInsert(any(), any()) } throws IOException("db locked")
110110

111111
val result = createRepository().saveSession(sessionResult())
@@ -116,7 +116,7 @@ class RankingRepositoryImplTest {
116116

117117
@Test
118118
fun `saveSession should return Error mapped from Throwable when insertSession throws`() =
119-
runTest {
119+
runTest(testDispatcher) {
120120
every { playerDataSource.findOrInsert(any(), any()) } returns
121121
SelectRankingFixtures.playerEntity()
122122
every {
@@ -131,7 +131,7 @@ class RankingRepositoryImplTest {
131131

132132
@Test
133133
fun `getTopRanking should map data source entries to domain RankingEntry list`() =
134-
runTest {
134+
runTest(testDispatcher) {
135135
every { quizSessionDataSource.selectRanking() } returns
136136
listOf(
137137
SelectRankingFixtures.leandroHardTopRow,
@@ -149,7 +149,7 @@ class RankingRepositoryImplTest {
149149

150150
@Test
151151
fun `getTopRanking should return Success with empty list when there are no sessions`() =
152-
runTest {
152+
runTest(testDispatcher) {
153153
every { quizSessionDataSource.selectRanking() } returns emptyList()
154154

155155
val result = createRepository().getTopRanking()
@@ -159,7 +159,7 @@ class RankingRepositoryImplTest {
159159

160160
@Test
161161
fun `getTopRanking should return Error mapped from Throwable when data source throws`() =
162-
runTest {
162+
runTest(testDispatcher) {
163163
every { quizSessionDataSource.selectRanking() } throws RuntimeException("boom")
164164

165165
val result = createRepository().getTopRanking()
@@ -170,7 +170,7 @@ class RankingRepositoryImplTest {
170170

171171
@Test
172172
fun `getTopRankingByPlayerName should forward player name to the data source`() =
173-
runTest {
173+
runTest(testDispatcher) {
174174
every { quizSessionDataSource.selectRankingByPlayerName(any()) } returns emptyList()
175175

176176
createRepository().getTopRankingByPlayerName("Leandro")
@@ -180,7 +180,7 @@ class RankingRepositoryImplTest {
180180

181181
@Test
182182
fun `getTopRankingByPlayerName should map entries to domain RankingEntry list`() =
183-
runTest {
183+
runTest(testDispatcher) {
184184
every { quizSessionDataSource.selectRankingByPlayerName(any()) } returns
185185
listOf(
186186
SelectRankingFixtures.leandroByNameRow,
@@ -200,8 +200,8 @@ class RankingRepositoryImplTest {
200200

201201
@Test
202202
fun `getTopRankingByPlayerName should return Error mapped from Throwable when data source throws`() =
203-
runTest {
204-
every { quizSessionDataSource.selectRankingByPlayerName(any()) } throws IOException("offline")
203+
runTest(testDispatcher) {
204+
every { quizSessionDataSource.selectRankingByPlayerName(any()) } throws IOException("io")
205205

206206
val result = createRepository().getTopRankingByPlayerName("Leandro")
207207

dynaquiz/composeApp/src/commonTest/kotlin/com/leanite/dynaquiz/core/domain/model/ScoreTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ScoreTest {
4545

4646
val result = logs.computeScore(ChallengeMode.Timed.Easy)
4747

48-
// 2 logs × (basePoints 2 + bonus 1/s): 12 + 7 = 19
48+
// 2 logs * (basePoints 2 + bonus 1/s): 12 + 7 = 19
4949
assertEquals(Score(19), result)
5050
}
5151

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.leanite.dynaquiz.core.ui.common
22

3+
import androidx.compose.runtime.Composable
34
import androidx.compose.ui.test.ExperimentalTestApi
45
import androidx.compose.ui.test.runComposeUiTest
56
import com.leanite.dynaquiz.core.ui.theme.DynaquizTheme
@@ -9,17 +10,23 @@ import com.leanite.dynaquiz.uitest.assertTextIsNotEnabled
910
import com.leanite.dynaquiz.uitest.clickOnText
1011
import kotlin.test.Test
1112
import kotlin.test.assertEquals
12-
import kotlin.test.assertTrue
1313

1414
@OptIn(ExperimentalTestApi::class)
1515
class GameButtonTest : UiTest() {
16+
17+
18+
@Composable
19+
private fun GameButtonContent(text: String, onClick: () -> Unit, enabled: Boolean = true) {
20+
DynaquizTheme {
21+
GameButton(text = text, onClick = onClick, enabled = enabled)
22+
}
23+
}
24+
1625
@Test
1726
fun `should render the provided text`() =
1827
runComposeUiTest {
1928
setContent {
20-
DynaquizTheme {
21-
GameButton(text = "PLAY", onClick = {})
22-
}
29+
GameButtonContent(text = "PLAY", onClick = {})
2330
}
2431

2532
assertTextIsDisplayed("PLAY")
@@ -30,9 +37,7 @@ class GameButtonTest : UiTest() {
3037
runComposeUiTest {
3138
var clicks = 0
3239
setContent {
33-
DynaquizTheme {
34-
GameButton(text = "PLAY", onClick = { clicks++ })
35-
}
40+
GameButtonContent(text = "PLAY", onClick = { clicks++ })
3641
}
3742

3843
clickOnText("PLAY")
@@ -44,9 +49,7 @@ class GameButtonTest : UiTest() {
4449
fun `should be disabled when enabled is false`() =
4550
runComposeUiTest {
4651
setContent {
47-
DynaquizTheme {
48-
GameButton(text = "PLAY", onClick = {}, enabled = false)
49-
}
52+
GameButtonContent(text = "PLAY", onClick = {}, enabled = false)
5053
}
5154

5255
assertTextIsNotEnabled("PLAY")
@@ -57,12 +60,10 @@ class GameButtonTest : UiTest() {
5760
runComposeUiTest {
5861
var clicks = 0
5962
setContent {
60-
DynaquizTheme {
61-
GameButton(text = "PLAY", onClick = { clicks++ }, enabled = false)
62-
}
63+
GameButtonContent(text = "PLAY", onClick = { clicks++ }, enabled = false)
6364
}
6465

6566
runCatching { clickOnText("PLAY") }
66-
assertTrue(clicks == 0)
67+
assertEquals(clicks, 0)
6768
}
6869
}
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.leanite.dynaquiz.core.ui.common
22

3+
import androidx.compose.runtime.Composable
34
import androidx.compose.ui.test.ExperimentalTestApi
45
import androidx.compose.ui.test.runComposeUiTest
56
import com.leanite.dynaquiz.core.ui.theme.DynaquizTheme
@@ -9,17 +10,22 @@ import com.leanite.dynaquiz.uitest.assertTextIsNotEnabled
910
import com.leanite.dynaquiz.uitest.clickOnText
1011
import kotlin.test.Test
1112
import kotlin.test.assertEquals
12-
import kotlin.test.assertTrue
1313

1414
@OptIn(ExperimentalTestApi::class)
1515
class GeneralActionButtonTest : UiTest() {
16+
17+
@Composable
18+
private fun GeneralActionButtonContent(text: String, onClick: () -> Unit, enabled: Boolean = true) {
19+
DynaquizTheme {
20+
GeneralActionButton(text = text, onClick = onClick, enabled = enabled)
21+
}
22+
}
23+
1624
@Test
1725
fun `should render the provided text`() =
1826
runComposeUiTest {
1927
setContent {
20-
DynaquizTheme {
21-
GeneralActionButton(text = "SAVE", onClick = {})
22-
}
28+
GeneralActionButtonContent(text = "SAVE", onClick = {})
2329
}
2430

2531
assertTextIsDisplayed("SAVE")
@@ -30,9 +36,7 @@ class GeneralActionButtonTest : UiTest() {
3036
runComposeUiTest {
3137
var clicks = 0
3238
setContent {
33-
DynaquizTheme {
34-
GeneralActionButton(text = "SAVE", onClick = { clicks++ })
35-
}
39+
GeneralActionButtonContent(text = "SAVE", onClick = { clicks++ })
3640
}
3741

3842
clickOnText("SAVE")
@@ -45,13 +49,11 @@ class GeneralActionButtonTest : UiTest() {
4549
runComposeUiTest {
4650
var clicks = 0
4751
setContent {
48-
DynaquizTheme {
49-
GeneralActionButton(text = "SAVE", onClick = { clicks++ }, enabled = false)
50-
}
52+
GeneralActionButtonContent(text = "SAVE", onClick = { clicks++ }, enabled = false)
5153
}
5254

5355
assertTextIsNotEnabled("SAVE")
5456
runCatching { clickOnText("SAVE") }
55-
assertTrue(clicks == 0)
57+
assertEquals(clicks, 0)
5658
}
5759
}

0 commit comments

Comments
 (0)