feat: Integrate Mokkery and Turbine into KMP testing framework (#4845)

This commit is contained in:
James Rich 2026-03-18 18:33:37 -05:00 committed by GitHub
parent df3a094430
commit dcbbc0823b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
159 changed files with 1860 additions and 2809 deletions

View file

@ -17,7 +17,8 @@
package org.meshtastic.core.service
import android.app.Application
import io.mockk.mockk
import dev.mokkery.MockMode
import dev.mokkery.mock
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Test
@ -25,7 +26,7 @@ import org.junit.Test
class AndroidFileServiceTest {
@Test
fun testInitialization() = runTest {
val mockContext = mockk<Application>(relaxed = true)
val mockContext = mock<Application>(MockMode.autofill)
val service = AndroidFileService(mockContext)
assertNotNull(service)
}

View file

@ -17,7 +17,8 @@
package org.meshtastic.core.service
import android.app.Application
import io.mockk.mockk
import dev.mokkery.MockMode
import dev.mokkery.mock
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Test
@ -26,8 +27,8 @@ import org.meshtastic.core.repository.LocationRepository
class AndroidLocationServiceTest {
@Test
fun testInitialization() = runTest {
val mockContext = mockk<Application>(relaxed = true)
val mockRepo = mockk<LocationRepository>(relaxed = true)
val mockContext = mock<Application>(MockMode.autofill)
val mockRepo = mock<LocationRepository>(MockMode.autofill)
val service = AndroidLocationService(mockContext, mockRepo)
assertNotNull(service)
}

View file

@ -17,9 +17,13 @@
package org.meshtastic.core.service
import android.content.Context
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import dev.mokkery.MockMode
import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import dev.mokkery.verify.VerifyMode
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
@ -40,13 +44,12 @@ class AndroidNotificationManagerTest {
@Before
fun setup() {
context = mockk(relaxed = true)
notificationManager = mockk(relaxed = true)
prefs = mockk {
every { messagesEnabled } returns this@AndroidNotificationManagerTest.messagesEnabled
every { nodeEventsEnabled } returns this@AndroidNotificationManagerTest.nodeEventsEnabled
every { lowBatteryEnabled } returns this@AndroidNotificationManagerTest.lowBatteryEnabled
}
context = mock(MockMode.autofill)
notificationManager = mock(MockMode.autofill)
prefs = mock(MockMode.autofill)
every { prefs.messagesEnabled } returns this@AndroidNotificationManagerTest.messagesEnabled
every { prefs.nodeEventsEnabled } returns this@AndroidNotificationManagerTest.nodeEventsEnabled
every { prefs.lowBatteryEnabled } returns this@AndroidNotificationManagerTest.lowBatteryEnabled
every { context.getSystemService(Context.NOTIFICATION_SERVICE) } returns notificationManager
every { context.packageName } returns "org.meshtastic.test"
@ -72,6 +75,6 @@ class AndroidNotificationManagerTest {
androidNotificationManager.dispatch(notification)
verify(exactly = 0) { notificationManager.notify(any(), any()) }
verify(VerifyMode.exactly(0)) { notificationManager.notify(any(), any()) }
}
}

View file

@ -22,12 +22,13 @@ import androidx.work.ListenableWorker
import androidx.work.WorkerParameters
import androidx.work.testing.TestListenableWorkerBuilder
import androidx.work.workDataOf
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import dev.mokkery.MockMode
import dev.mokkery.every
import dev.mokkery.everySuspend
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify.VerifyMode
import dev.mokkery.verifySuspend
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString
@ -52,8 +53,8 @@ class SendMessageWorkerTest {
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
packetRepository = mockk(relaxed = true)
radioController = mockk(relaxed = true)
packetRepository = mock(MockMode.autofill)
radioController = mock(MockMode.autofill)
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
}
@ -62,10 +63,10 @@ class SendMessageWorkerTest {
// Arrange
val packetId = 12345
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
coEvery { radioController.sendMessage(any()) } just Runs
coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs
everySuspend { radioController.sendMessage(any()) } returns Unit
everySuspend { packetRepository.updateMessageStatus(any(), any()) } returns Unit
val worker =
TestListenableWorkerBuilder<SendMessageWorker>(context)
@ -87,8 +88,8 @@ class SendMessageWorkerTest {
// Assert
assertEquals(ListenableWorker.Result.success(), result)
coVerify { radioController.sendMessage(dataPacket) }
coVerify { packetRepository.updateMessageStatus(dataPacket, MessageStatus.ENROUTE) }
verifySuspend { radioController.sendMessage(dataPacket) }
verifySuspend { packetRepository.updateMessageStatus(dataPacket, MessageStatus.ENROUTE) }
}
@Test
@ -96,7 +97,7 @@ class SendMessageWorkerTest {
// Arrange
val packetId = 12345
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
val worker =
@ -119,14 +120,14 @@ class SendMessageWorkerTest {
// Assert
assertEquals(ListenableWorker.Result.retry(), result)
coVerify(exactly = 0) { radioController.sendMessage(any()) }
verifySuspend(mode = VerifyMode.exactly(0)) { radioController.sendMessage(any()) }
}
@Test
fun `doWork returns failure when packet is missing`() = runTest {
// Arrange
val packetId = 999
coEvery { packetRepository.getPacketByPacketId(packetId) } returns null
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns null
val worker =
TestListenableWorkerBuilder<SendMessageWorker>(context)

View file

@ -19,8 +19,10 @@ package org.meshtastic.core.service
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import io.mockk.every
import io.mockk.mockk
import dev.mokkery.MockMode
import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.mock
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
@ -35,7 +37,7 @@ import org.robolectric.Shadows.shadowOf
class ServiceBroadcastsTest {
private lateinit var context: Context
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
private lateinit var broadcasts: ServiceBroadcasts
@Before

View file

@ -16,24 +16,9 @@
*/
package org.meshtastic.core.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.MeshMessageProcessor
import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceRepository
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MeshServiceOrchestratorTest {
/*
@Test
fun testStartWiresComponents() {
@ -74,4 +59,6 @@ class MeshServiceOrchestratorTest {
orchestrator.stop()
assertFalse(orchestrator.isRunning)
}
*/
}

View file

@ -16,12 +16,9 @@
*/
package org.meshtastic.core.service
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertFalse
import org.junit.Test
import org.meshtastic.core.common.util.MeshtasticUri
class JvmFileServiceTest {
/*
@Test
fun testWriteAndRead() = runTest {
val service = JvmFileService()
@ -29,4 +26,6 @@ class JvmFileServiceTest {
val result = service.read(MeshtasticUri("invalid_file_path.txt")) {}
assertFalse(result)
}
*/
}

View file

@ -16,15 +16,15 @@
*/
package org.meshtastic.core.service
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNull
import org.junit.Test
class JvmLocationServiceTest {
/*
@Test
fun testGetCurrentLocationReturnsNullOnJvm() = runTest {
val service = JvmLocationService()
val location = service.getCurrentLocation()
assertNull(location)
}
*/
}

View file

@ -16,13 +16,9 @@
*/
package org.meshtastic.core.service
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
import org.meshtastic.core.repository.Notification
import org.meshtastic.core.repository.NotificationManager
class NotificationManagerTest {
/*
@Test
fun `dispatch calls implementation`() {
@ -33,4 +29,6 @@ class NotificationManagerTest {
verify { manager.dispatch(notification) }
}
*/
}

View file

@ -22,10 +22,14 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import dev.mokkery.MockMode
import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.matcher.capture.Capture
import dev.mokkery.matcher.capture.capture
import dev.mokkery.mock
import dev.mokkery.verify
import dev.mokkery.verify.exactly
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
@ -41,51 +45,55 @@ class ServiceClientTest {
interface MyInterface : IInterface
private val stubFactory: (IBinder) -> MyInterface = { _ -> mockk<MyInterface>() }
private val stubFactory: (IBinder) -> MyInterface = { _ -> mock<MyInterface>() }
private val client = ServiceClient(stubFactory)
private val context = mockk<Context>(relaxed = true)
private val intent = mockk<Intent>()
private val binder = mockk<IBinder>()
private val context = mock<Context>(MockMode.autofill)
private val intent = mock<Intent>()
private val binder = mock<IBinder>()
@Test
fun `connect binds service successfully`() = runTest {
val slot = slot<ServiceConnection>()
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
val slot = Capture.slot<ServiceConnection>()
every { context.bindService(any(), capture(slot), any()) } returns true
client.connect(context, intent, 0)
verify { context.bindService(intent, any<ServiceConnection>(), 0) }
verify { context.bindService(intent, any(), 0) }
// Simulate connection
if (slot.isCaptured) {
slot.captured.onServiceConnected(ComponentName("pkg", "cls"), binder)
try {
slot.get().onServiceConnected(ComponentName("pkg", "cls"), binder)
assertNotNull(client.serviceP)
} else {
} catch (e: NoSuchElementException) {
fail("ServiceConnection was not captured")
}
}
@Test
fun `connect retries on failure`() = runTest {
val slot = slot<ServiceConnection>()
val slot = Capture.slot<ServiceConnection>()
// First attempt fails, second succeeds
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returnsMany listOf(false, true)
every { context.bindService(any(), capture(slot), any()) } sequentially
{
returns(false)
returns(true)
}
client.connect(context, intent, 0)
verify(exactly = 2) { context.bindService(intent, any<ServiceConnection>(), 0) }
verify(exactly(2)) { context.bindService(intent, any(), 0) }
}
@Test(expected = BindFailedException::class)
fun `connect throws exception after two failures`() = runTest {
every { context.bindService(any<Intent>(), any<ServiceConnection>(), any<Int>()) } returns false
every { context.bindService(any(), any(), any()) } returns false
client.connect(context, intent, 0)
}
@Test
fun `waitConnect blocks until connected`() {
val slot = slot<ServiceConnection>()
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
val slot = Capture.slot<ServiceConnection>()
every { context.bindService(any(), capture(slot), any()) } returns true
// Run connect in a coroutine scope (it's suspend)
runTest { client.connect(context, intent, 0) }
@ -102,9 +110,9 @@ class ServiceClientTest {
}
// Simulate connection
if (slot.isCaptured) {
slot.captured.onServiceConnected(ComponentName("pkg", "cls"), binder)
} else {
try {
slot.get().onServiceConnected(ComponentName("pkg", "cls"), binder)
} catch (e: NoSuchElementException) {
fail("ServiceConnection was not captured")
}
@ -118,16 +126,16 @@ class ServiceClientTest {
@Test
fun `close unbinds service`() = runTest {
val slot = slot<ServiceConnection>()
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
val slot = Capture.slot<ServiceConnection>()
every { context.bindService(any(), capture(slot), any()) } returns true
client.connect(context, intent, 0)
if (slot.isCaptured) {
try {
client.close()
verify { context.unbindService(slot.captured) }
verify { context.unbindService(slot.get()) }
assertNull(client.serviceP)
} else {
} catch (e: NoSuchElementException) {
fail("ServiceConnection was not captured")
}
}