Skip to content

Commit 2079e90

Browse files
add coroutines tests
1 parent 3bbe6f1 commit 2079e90

File tree

3 files changed

+168
-5
lines changed

3 files changed

+168
-5
lines changed

Tutorial2-1Unit-Testing/build.gradle.kts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ dependencies {
6565
implementation("androidx.compose.material3:material3")
6666

6767
testImplementation("junit:junit:4.13.2")
68-
testImplementation ("org.junit.jupiter:junit-jupiter-api:5.8.2")
69-
testImplementation ("org.junit.jupiter:junit-jupiter-engine:5.8.2")
68+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0")
69+
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.0")
7070

71-
testImplementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2")
72-
testImplementation ("io.mockk:mockk-android:1.13.2")
73-
testImplementation ("com.google.truth:truth:1.1.3")
71+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2")
72+
testImplementation("io.mockk:mockk-android:1.13.2")
73+
testImplementation("com.google.truth:truth:1.1.3")
7474

7575
androidTestImplementation("androidx.test.ext:junit:1.1.5")
7676
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.smarttoolfactory.tutorial2_1unit_testing.coroutines
2+
3+
import kotlinx.coroutines.CompletableDeferred
4+
import kotlinx.coroutines.ExperimentalCoroutinesApi
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.launch
7+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
8+
import kotlinx.coroutines.test.advanceUntilIdle
9+
import kotlinx.coroutines.test.runTest
10+
import org.junit.jupiter.api.Assertions.assertEquals
11+
import org.junit.jupiter.api.Assertions.assertFalse
12+
import org.junit.jupiter.api.Assertions.assertTrue
13+
import org.junit.jupiter.api.Test
14+
15+
@OptIn(ExperimentalCoroutinesApi::class)
16+
class CoroutinesTest1 {
17+
18+
@Test
19+
fun standardTest() = runTest {
20+
val userRepo = UserRepository()
21+
22+
launch { userRepo.registerUsers("Alice") }
23+
launch { userRepo.registerUsers("Bob") }
24+
25+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
26+
}
27+
28+
@Test
29+
fun standardTestWithJoin() = runTest {
30+
val userRepo = UserRepository()
31+
32+
val job1 = launch { userRepo.registerUsers("Alice") }
33+
val job2 = launch { userRepo.registerUsers("Bob") }
34+
35+
job1.join()
36+
job2.join()
37+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
38+
}
39+
40+
// 🔥 This test passes with registerUserAsync too
41+
@Test
42+
fun standardTest2() = runTest {
43+
val userRepo = UserRepository()
44+
45+
launch { userRepo.registerUsers("Alice") }
46+
launch { userRepo.registerUsers("Bob") }
47+
advanceUntilIdle() // Yields to perform the registrations
48+
49+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
50+
}
51+
52+
/*
53+
UnconfinedTestDispatcher
54+
When new coroutines are started on an UnconfinedTestDispatcher,
55+
they are started eagerly on the current thread. This means that they’ll
56+
start running immediately, without waiting for their coroutine builder to return.
57+
In many cases, this dispatching behavior results in simpler test code,
58+
as you don’t need to manually yield the test thread to let new coroutines run.
59+
60+
However, this behavior is different from what you’ll see in production with
61+
non-test dispatchers. If your test focuses on concurrency,
62+
prefer using StandardTestDispatcher instead.
63+
*/
64+
65+
66+
// 🔥 This test fails with registerUserAsync(delay)
67+
@Test
68+
fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) {
69+
val userRepo = UserRepository()
70+
71+
launch { userRepo.registerUsers("Alice") }
72+
launch { userRepo.registerUsers("Bob") }
73+
74+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
75+
}
76+
77+
@Test
78+
fun unconfinedTest2() = runTest(UnconfinedTestDispatcher()) {
79+
val userRepo = UserRepository()
80+
81+
launch { userRepo.registerUserAsync("Alice") }
82+
launch { userRepo.registerUserAsync("Bob") }
83+
// 🔥 Need to call for past to test
84+
advanceUntilIdle()
85+
86+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
87+
}
88+
89+
/*
90+
Remember that UnconfinedTestDispatcher starts new coroutines eagerly,
91+
but this doesn’t mean that it’ll run them to completion eagerly as well.
92+
If the new coroutine suspends, other coroutines will resume executing.
93+
94+
For example, the new coroutine launched within this test will register Alice,
95+
but then it suspends when delay is called. This lets the top-level coroutine
96+
proceed with the assertion, and the test fails as Bob is not registered yet:
97+
*/
98+
@Test
99+
fun yieldingTest() = runTest(UnconfinedTestDispatcher()) {
100+
val userRepo = UserRepository()
101+
102+
launch {
103+
userRepo.registerUsers("Alice")
104+
delay(10L)
105+
userRepo.registerUsers("Bob")
106+
}
107+
108+
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails
109+
}
110+
111+
/*
112+
Like Dispatchers.Unconfined, this one does not provide guarantees about the execution
113+
order when several coroutines are queued in this dispatcher.
114+
However, we ensure that the launch and async blocks at the top level of
115+
runTest are entered eagerly. This allows launching child coroutines and not calling
116+
runCurrent for them to start executing.
117+
118+
*/
119+
@Test
120+
fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
121+
println("🍏before launch")
122+
123+
var entered = false
124+
val deferred = CompletableDeferred<Unit>()
125+
var completed = false
126+
127+
launch {
128+
entered = true
129+
deferred.await()
130+
completed = true
131+
println("🚀 inside launch")
132+
}
133+
134+
println("🍎 after launch")
135+
assertTrue(entered) // `entered = true` already executed.
136+
assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
137+
deferred.complete(Unit) // resume the coroutine.
138+
assertTrue(completed) // now the child coroutine is immediately completed.
139+
// ✅ Passes
140+
}
141+
142+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.smarttoolfactory.tutorial2_1unit_testing.coroutines
2+
3+
import kotlinx.coroutines.delay
4+
5+
class UserRepository {
6+
private val users = mutableListOf<String>()
7+
8+
fun registerUsers(user: String) {
9+
users.add(user)
10+
}
11+
12+
suspend fun registerUserAsync(user: String) {
13+
delay(100)
14+
users.add(user)
15+
}
16+
17+
fun getAllUsers(): MutableList<String> {
18+
return users
19+
}
20+
}
21+

0 commit comments

Comments
 (0)