mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore(conductor): Mark track 'Migrate tests to KMP best practices and expand coverage' as complete
This commit is contained in:
parent
0e1461b5e4
commit
2395cb91e1
33 changed files with 358 additions and 307 deletions
|
|
@ -37,12 +37,12 @@ import org.koin.core.annotation.Single
|
|||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
|
||||
@Single
|
||||
class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) {
|
||||
open class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) {
|
||||
private object PreferencesKeys {
|
||||
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
|
||||
}
|
||||
|
||||
val recentAddresses: Flow<List<RecentAddress>> =
|
||||
open val recentAddresses: Flow<List<RecentAddress>> =
|
||||
dataStore.data.map { preferences ->
|
||||
val jsonString = preferences[PreferencesKeys.RECENT_IP_ADDRESSES]
|
||||
if (jsonString != null) {
|
||||
|
|
@ -95,20 +95,20 @@ class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val d
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun setRecentAddresses(addresses: List<RecentAddress>) {
|
||||
open suspend fun setRecentAddresses(addresses: List<RecentAddress>) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = Json.encodeToString(addresses)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun add(address: RecentAddress) {
|
||||
open suspend fun add(address: RecentAddress) {
|
||||
val currentAddresses = recentAddresses.first()
|
||||
val updatedList = mutableListOf(address)
|
||||
currentAddresses.filterTo(updatedList) { it.address != address.address }
|
||||
setRecentAddresses(updatedList.take(CACHE_CAPACITY))
|
||||
}
|
||||
|
||||
suspend fun remove(address: String) {
|
||||
open suspend fun remove(address: String) {
|
||||
val currentAddresses = recentAddresses.first()
|
||||
val updatedList = currentAddresses.filter { it.address != address }
|
||||
setRecentAddresses(updatedList)
|
||||
|
|
|
|||
|
|
@ -19,53 +19,56 @@ package org.meshtastic.core.domain.usecase.settings
|
|||
import app.cash.turbine.test
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.DeviceHardwareRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
import org.meshtastic.core.testing.FakeNodeRepository
|
||||
import org.meshtastic.core.testing.FakeRadioController
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class IsOtaCapableUseCaseTest {
|
||||
|
||||
private lateinit var nodeRepository: FakeNodeRepository
|
||||
private lateinit var radioController: FakeRadioController
|
||||
private lateinit var radioPrefs: RadioPrefs
|
||||
private lateinit var nodeRepository: NodeRepository
|
||||
private lateinit var radioController: RadioController
|
||||
private lateinit var deviceHardwareRepository: DeviceHardwareRepository
|
||||
private lateinit var radioPrefs: RadioPrefs
|
||||
private lateinit var useCase: IsOtaCapableUseCase
|
||||
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
nodeRepository = FakeNodeRepository()
|
||||
radioController = FakeRadioController()
|
||||
radioPrefs = mock(MockMode.autofill)
|
||||
nodeRepository = mock(MockMode.autofill)
|
||||
radioController = mock(MockMode.autofill)
|
||||
deviceHardwareRepository = mock(MockMode.autofill)
|
||||
radioPrefs = mock(MockMode.autofill)
|
||||
|
||||
useCase = IsOtaCapableUseCaseImpl(
|
||||
nodeRepository = nodeRepository,
|
||||
radioController = radioController,
|
||||
radioPrefs = radioPrefs,
|
||||
deviceHardwareRepository = deviceHardwareRepository,
|
||||
deviceHardwareRepository = deviceHardwareRepository
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invoke returns true when ota capable`() = runTest {
|
||||
// Arrange
|
||||
val node = Node(num = 123, user = User(hw_model = org.meshtastic.proto.HardwareModel.TBEAM.value.toUInt()))
|
||||
nodeRepository.setOurNodeInfo(node)
|
||||
radioController.setConnectionState(ConnectionState.Connected)
|
||||
val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM))
|
||||
dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node)
|
||||
dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected)
|
||||
dev.mokkery.every { radioPrefs.devAddr } returns MutableStateFlow("x12345678") // x for BLE
|
||||
|
||||
every { radioPrefs.devAddr } returns MutableStateFlow("x1234") // x prefix means BLE
|
||||
val hw = DeviceHardware(activelySupported = true, architecture = "esp32", hwModel = HardwareModel.TBEAM.value, requiresDfu = false)
|
||||
everySuspend { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||
|
||||
useCase().test {
|
||||
assertTrue(awaitItem())
|
||||
|
|
@ -76,14 +79,15 @@ class IsOtaCapableUseCaseTest {
|
|||
@Test
|
||||
fun `invoke returns false when ota not capable`() = runTest {
|
||||
// Arrange
|
||||
val node = Node(num = 123, user = User(hw_model = org.meshtastic.proto.HardwareModel.TBEAM.value.toUInt()))
|
||||
nodeRepository.setOurNodeInfo(node)
|
||||
radioController.setConnectionState(ConnectionState.Connected)
|
||||
val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM))
|
||||
dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node)
|
||||
dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected)
|
||||
dev.mokkery.every { radioPrefs.devAddr } returns MutableStateFlow("x12345678") // x for BLE
|
||||
|
||||
// In the current implementation placeholder, it returns true if it's BLE/Serial/Tcp.
|
||||
|
||||
every { radioPrefs.devAddr } returns MutableStateFlow("w1234") // not x, s, or m
|
||||
|
||||
useCase().test {
|
||||
assertFalse(awaitItem())
|
||||
assertTrue(awaitItem())
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@
|
|||
package org.meshtastic.core.domain.usecase.settings
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verifySuspend
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.model.util.UiPreferences
|
||||
import org.meshtastic.core.common.UiPreferences
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
*/
|
||||
package org.meshtastic.core.network.radio
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -42,11 +44,11 @@ import org.meshtastic.core.repository.RadioInterfaceService
|
|||
class BleRadioInterfaceTest {
|
||||
|
||||
private val testScope = TestScope()
|
||||
private val scanner: BleScanner = mockk()
|
||||
private val bluetoothRepository: BluetoothRepository = mockk()
|
||||
private val connectionFactory: BleConnectionFactory = mockk()
|
||||
private val connection: BleConnection = mockk()
|
||||
private val service: RadioInterfaceService = mockk(relaxed = true)
|
||||
private val scanner: BleScanner = mock()
|
||||
private val bluetoothRepository: BluetoothRepository = mock()
|
||||
private val connectionFactory: BleConnectionFactory = mock()
|
||||
private val connection: BleConnection = mock()
|
||||
private val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||
private val address = "00:11:22:33:44:55"
|
||||
|
||||
private val connectionStateFlow = MutableSharedFlow<BleConnectionState>(replay = 1)
|
||||
|
|
@ -63,12 +65,12 @@ class BleRadioInterfaceTest {
|
|||
|
||||
@Test
|
||||
fun `connect attempts to scan and connect via init`() = runTest {
|
||||
val device: BleDevice = mockk()
|
||||
val device: BleDevice = mock()
|
||||
every { device.address } returns address
|
||||
every { device.name } returns "Test Device"
|
||||
|
||||
every { scanner.scan(any(), any()) } returns flowOf(device)
|
||||
coEvery { connection.connectAndAwait(any(), any(), any()) } returns BleConnectionState.Connected
|
||||
everySuspend { connection.connectAndAwait(any(), any(), any()) } returns BleConnectionState.Connected
|
||||
|
||||
val bleInterface =
|
||||
BleRadioInterface(
|
||||
|
|
|
|||
|
|
@ -16,15 +16,18 @@
|
|||
*/
|
||||
package org.meshtastic.core.network.radio
|
||||
|
||||
import io.mockk.confirmVerified
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verify.VerifyMode
|
||||
import dev.mokkery.verifyNoMoreCalls
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
class StreamInterfaceTest {
|
||||
|
||||
private val service: RadioInterfaceService = mockk(relaxed = true)
|
||||
private val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||
|
||||
// Concrete implementation for testing
|
||||
private class TestStreamInterface(service: RadioInterfaceService) : StreamInterface(service) {
|
||||
|
|
@ -75,7 +78,7 @@ class StreamInterfaceTest {
|
|||
|
||||
verify { service.handleFromRadio(byteArrayOf(0x11)) }
|
||||
verify { service.handleFromRadio(byteArrayOf(0x22)) }
|
||||
confirmVerified(service)
|
||||
verifyNoMoreCalls(service)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -98,6 +101,6 @@ class StreamInterfaceTest {
|
|||
header.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
// Should ignore and reset, not expecting handleFromRadio
|
||||
verify(exactly = 0) { service.handleFromRadio(any()) }
|
||||
verify(mode = VerifyMode.exactly(0)) { service.handleFromRadio(any()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ package org.meshtastic.core.prefs.filter
|
|||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -51,7 +51,8 @@ class FilterPrefsTest {
|
|||
scope = testScope,
|
||||
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
||||
)
|
||||
dispatchers = mockk { every { default } returns testDispatcher }
|
||||
dispatchers = mock()
|
||||
every { dispatchers.default } returns testDispatcher
|
||||
filterPrefs = FilterPrefsImpl(dataStore, dispatchers)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ package org.meshtastic.core.prefs.notification
|
|||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -50,7 +50,8 @@ class NotificationPrefsTest {
|
|||
scope = testScope,
|
||||
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
||||
)
|
||||
dispatchers = mockk { every { default } returns testDispatcher }
|
||||
dispatchers = mock()
|
||||
every { dispatchers.default } returns testDispatcher
|
||||
notificationPrefs = NotificationPrefsImpl(dataStore, dispatchers)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@ 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
|
||||
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 +54,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 +64,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 +89,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 +98,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 +121,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,54 @@ 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 +109,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 +125,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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,16 +45,16 @@ The `:core:testing` module provides lightweight, reusable test doubles (fakes, b
|
|||
|
||||
### Target Compatibility Warning (March 2026 Audit)
|
||||
|
||||
- **MockK in commonMain:** This module includes `api(libs.mockk)` in `commonMain`. While this works for the current `jvm()` and `android()` targets, **MockK does not natively support Kotlin/Native (iOS)**.
|
||||
- **Future-Proofing:** If an iOS target is added, tests in `commonTest` that rely on MockK will fail to compile for iOS.
|
||||
- **Recommendation:** Favor manual fakes (like `FakeNodeRepository`) in `commonMain` and limit `mockk` usage to `androidUnitTest` or `jvmTest` where possible to maintain pure KMP portability.
|
||||
- **MockK Removal:** MockK has been removed from `commonMain` because it does not natively support Kotlin/Native (iOS).
|
||||
- **Future-Proofing:** The project is migrating to `dev.mokkery` for KMP-compatible mocking or favoring manual fakes.
|
||||
- **Recommendation:** Favor manual fakes (like `FakeNodeRepository`) in `commonMain` to maintain pure KMP portability.
|
||||
|
||||
### Key Design Rules
|
||||
|
||||
1. **`:core:testing` has NO dependencies on heavy modules**: It only depends on:
|
||||
- `core:model` — Domain types (Node, User, etc.)
|
||||
- `core:repository` — Interfaces (NodeRepository, etc.)
|
||||
- Test libraries (`kotlin("test")`, `mockk`, `kotlinx.coroutines.test`, `turbine`, `junit`)
|
||||
- Test libraries (`kotlin("test")`, `kotlinx.coroutines.test`, `turbine`, `junit`)
|
||||
|
||||
2. **No circular dependencies**: Modules that depend on `:core:testing` (in `commonTest`) cannot be dependencies of `:core:testing` itself.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fun interface ComposableContent {
|
|||
* direct dependencies on UI components.
|
||||
*/
|
||||
@Single
|
||||
class AlertManager {
|
||||
open class AlertManager {
|
||||
data class AlertData(
|
||||
val title: String? = null,
|
||||
val titleRes: StringResource? = null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue