Skip to content

Commit 4013fcd

Browse files
committed
add IOMockHelper for deterministic IO completion
1 parent 001ee12 commit 4013fcd

File tree

1 file changed

+83
-0
lines changed
  • OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks

1 file changed

+83
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.onesignal.mocks
2+
3+
import com.onesignal.common.threading.suspendifyOnIO
4+
import com.onesignal.common.threading.suspendifyWithCompletion
5+
import io.kotest.core.listeners.AfterSpecListener
6+
import io.kotest.core.listeners.BeforeSpecListener
7+
import io.kotest.core.listeners.BeforeTestListener
8+
import io.kotest.core.listeners.TestListener
9+
import io.kotest.core.spec.Spec
10+
import io.kotest.core.test.TestCase
11+
import io.mockk.every
12+
import io.mockk.mockkStatic
13+
import io.mockk.unmockkStatic
14+
import kotlinx.coroutines.CompletableDeferred
15+
import kotlinx.coroutines.runBlocking
16+
17+
/**
18+
* Test helper that makes OneSignal’s `suspendifyOnIO` behavior deterministic in unit tests.
19+
* Can be helpful to speed up unit tests by replacing all delay(x) or Thread.sleep(x).
20+
*
21+
* In production, `suspendifyOnIO` launches work on background threads and returns immediately.
22+
* This causes tests to require arbitrary delays (e.g., delay(50)) to wait for async work to finish.
23+
*
24+
* This helper avoids that by:
25+
* - Replacing Dispatchers.Main with a test dispatcher
26+
* - Mocking `suspendifyOnIO` so its block runs immediately
27+
* - Completing a `CompletableDeferred` when the async block finishes
28+
* - Providing `awaitIO()` so tests can explicitly wait for all IO work without sleeps
29+
*
30+
* Usage in a Kotest spec:
31+
*
32+
* class InAppMessagesManagerTests : FunSpec({
33+
*
34+
* // register to access awaitIO()
35+
* listener(IOMockHelper)
36+
* ...
37+
*
38+
* test("xyz") {
39+
* iamManager.start() // start() calls suspendOnIO
40+
* awaitIO() // wait for background work deterministically
41+
* ...
42+
* }
43+
*/
44+
object IOMockHelper : BeforeSpecListener, AfterSpecListener, BeforeTestListener, TestListener {
45+
46+
private var ioWaiter: CompletableDeferred<Unit> = CompletableDeferred()
47+
48+
/**
49+
* Wait for the current suspendifyOnIO work to finish.
50+
* Can be called from tests instead of delay/Thread.sleep.
51+
*/
52+
fun awaitIO() {
53+
if (!ioWaiter.isCompleted) {
54+
runBlocking {
55+
ioWaiter.await()
56+
}
57+
}
58+
ioWaiter = CompletableDeferred()
59+
}
60+
61+
override suspend fun beforeSpec(spec: Spec) {
62+
// ThreadUtilsKt = file that contains suspendifyOnIO
63+
mockkStatic("com.onesignal.common.threading.ThreadUtilsKt")
64+
65+
every { suspendifyOnIO(any<suspend () -> Unit>()) } answers {
66+
val block = firstArg<suspend () -> Unit>()
67+
suspendifyWithCompletion(
68+
useIO = true,
69+
block = block,
70+
onComplete = { ioWaiter.complete(Unit) },
71+
)
72+
}
73+
}
74+
75+
override suspend fun beforeTest(testCase: TestCase) {
76+
// fresh waiter for each test
77+
ioWaiter = CompletableDeferred()
78+
}
79+
80+
override suspend fun afterSpec(spec: Spec) {
81+
unmockkStatic("com.onesignal.common.threading.ThreadUtilsKt")
82+
}
83+
}

0 commit comments

Comments
 (0)