Add mock tests to GLS

This commit is contained in:
Sylwester Zieliński
2023-06-06 16:52:27 +02:00
parent 8e0348565e
commit 2a66654c33
16 changed files with 234 additions and 53 deletions

View File

@@ -79,4 +79,6 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.activity.compose)
implementation(libs.nordic.blek.client)
}

View File

@@ -30,7 +30,8 @@
~ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="no.nordicsemi.android.dfu" />
@@ -44,12 +45,14 @@
<application
android:name=".NrfToolboxApplication"
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/NordicTheme">
android:theme="@style/NordicTheme"
android:dataExtractionRules="@xml/data_extraction_rules"
tools:targetApi="s">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</device-transfer>
</data-extraction-rules>

View File

@@ -125,7 +125,7 @@ internal class BPSViewModel @Inject constructor(
private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
_state.value = _state.value.copy(deviceName = device.name)
logger = NordicBlekLogger(context, stringConst.APP_NAME, "BPS", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "BPS", device.address)
client = device.connect(context, logger = logger)

View File

@@ -92,7 +92,7 @@ class CGMRepository @Inject constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "CGM", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "CGM", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(CGMService::class.java, device)
}

View File

@@ -92,7 +92,7 @@ class CSCRepository @Inject constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "CSC", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "CSC", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(CSCService::class.java, device)
}

View File

@@ -66,5 +66,12 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.service)
testImplementation(libs.junit4)
testImplementation("io.mockk:mockk:1.13.5")
implementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
testImplementation("org.slf4j:slf4j-simple:2.0.5")
testImplementation("org.robolectric:robolectric:4.10.3")
}

View File

@@ -38,6 +38,11 @@ class GlsServer @Inject constructor(
private val scope: CoroutineScope
) {
lateinit var glsCharacteristic: BleServerGattCharacteristic
lateinit var glsContextCharacteristic: BleServerGattCharacteristic
lateinit var racpCharacteristic: BleServerGattCharacteristic
lateinit var batteryLevelCharacteristic: BleServerGattCharacteristic
private val records = listOf(
byteArrayOf(0x07, 0x00, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x05, 0x00, 0x00, 0x26, 0xD2.toByte(), 0x11),
byteArrayOf(0x07, 0x01, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x08, 0x00, 0x00, 0x3D, 0xD2.toByte(), 0x11),
@@ -48,7 +53,13 @@ class GlsServer @Inject constructor(
private val racp = byteArrayOf(0x06, 0x00, 0x01, 0x01)
fun start(context: Context) = scope.launch {
fun start(
context: Context,
device: MockServerDevice = MockServerDevice(
name = "GLS Server",
address = "55:44:33:22:11"
),
) = scope.launch {
val gmCharacteristic = BleServerGattCharacteristicConfig(
GLUCOSE_MEASUREMENT_CHARACTERISTIC,
listOf(BleGattProperty.PROPERTY_NOTIFY),
@@ -85,11 +96,6 @@ class GlsServer @Inject constructor(
listOf(batteryLevelCharacteristic)
)
val device = MockServerDevice(
name = "GLS Server",
address = "55:44:33:22:11"
)
val server = BleGattServer.create(
context = context,
config = arrayOf(serviceConfig, batteryService),
@@ -107,16 +113,20 @@ class GlsServer @Inject constructor(
}
private fun setUpConnection(connection: BluetoothGattServerConnection) {
val glsService = connection.services.findService(GLS_SERVICE_UUID)!!
glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!!
glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!!
racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!!
batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
startGlsService(connection)
startBatteryService(connection)
// startBatteryService(connection)
}
private fun startGlsService(connection: BluetoothGattServerConnection) {
val glsService = connection.services.findService(GLS_SERVICE_UUID)!!
val glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!!
val glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!!
val racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!!
racpCharacteristic.value
.filter { it.isNotEmpty() }
.onEach {
@@ -150,9 +160,6 @@ class GlsServer @Inject constructor(
}
private fun startBatteryService(connection: BluetoothGattServerConnection) {
val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!!
val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!!
scope.launch {
repeat(100) {
batteryLevelCharacteristic.setValue(byteArrayOf(0x61))

View File

@@ -105,11 +105,11 @@ internal class GLSViewModel @Inject constructor(
private val stringConst: StringConst
) : ViewModel() {
private lateinit var client: BleGattClient
internal lateinit var client: BleGattClient
private lateinit var logger: NordicBlekLogger
private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic
private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
internal lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic
internal lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic
private val _state = MutableStateFlow(GLSViewState())
val state = _state.asStateFlow()
@@ -117,6 +117,8 @@ internal class GLSViewModel @Inject constructor(
private val highestSequenceNumber
get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1
fun test() = 2
init {
navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID))
@@ -125,7 +127,7 @@ internal class GLSViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun handleResult(result: NavigationResult<ServerDevice>) {
internal fun handleResult(result: NavigationResult<ServerDevice>) {
when (result) {
is NavigationResult.Cancelled -> navigationManager.navigateUp()
is NavigationResult.Success -> onDeviceSelected(result.value)
@@ -166,7 +168,7 @@ internal class GLSViewModel @Inject constructor(
private fun startGattClient(device: ServerDevice) = viewModelScope.launch {
_state.value = _state.value.copy(deviceName = device.name)
logger = NordicBlekLogger(context, stringConst.APP_NAME, "GLS", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "GLS", device.address)
client = device.connect(context, logger = logger)
@@ -194,7 +196,7 @@ internal class GLSViewModel @Inject constructor(
client.disconnect()
}
private fun logAnalytics(connectionState: GattConnectionStateWithStatus) {
internal fun logAnalytics(connectionState: GattConnectionStateWithStatus) {
if (connectionState.state == GattConnectionState.STATE_CONNECTED) {
analytics.logEvent(ProfileConnectedEvent(Profile.GLS))
}
@@ -318,7 +320,7 @@ internal class GLSViewModel @Inject constructor(
try {
recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)
} catch (e: Exception) {
e.printStackTrace()
_state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED)
}
}
}

View File

@@ -1,20 +0,0 @@
package no.nordicsemi.android.gls
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,158 @@
package no.nordicsemi.android.gls
import android.content.Context
import io.mockk.coJustRun
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit4.MockKRule
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.spyk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import no.nordicsemi.android.analytics.AppAnalytics
import no.nordicsemi.android.common.logger.NordicBlekLogger
import no.nordicsemi.android.common.navigation.NavigationResult
import no.nordicsemi.android.common.navigation.Navigator
import no.nordicsemi.android.gls.data.WorkingMode
import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
import no.nordicsemi.android.kotlin.ble.client.main.service.BleGattCharacteristic
import no.nordicsemi.android.kotlin.ble.core.MockServerDevice
import no.nordicsemi.android.kotlin.ble.core.ServerDevice
import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState
import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus
import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus
import no.nordicsemi.android.ui.view.StringConst
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowBluetoothGatt
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(RobolectricTestRunner::class)
internal class GLSViewModelTest {
@get:Rule
val mockkRule = MockKRule(this)
@RelaxedMockK
lateinit var navigator: Navigator
@RelaxedMockK
lateinit var analytics: AppAnalytics
@MockK
lateinit var stringConst: StringConst
@RelaxedMockK
lateinit var context: Context
@RelaxedMockK
lateinit var logger: NordicBlekLogger
@MockK
lateinit var characteristic: BleGattCharacteristic
lateinit var viewModel: GLSViewModel
lateinit var glsServer: GlsServer
private val device = MockServerDevice(
name = "GLS Server",
address = "55:44:33:22:11"
)
@Before
fun setUp() {
Dispatchers.setMain(UnconfinedTestDispatcher())
}
@After
fun release() {
Dispatchers.resetMain()
}
@Before
fun before() {
runBlocking {
viewModel = spyk(GLSViewModel(context, navigator, analytics, stringConst))
glsServer = GlsServer(CoroutineScope(UnconfinedTestDispatcher()))
glsServer.start(spyk(), device)
}
}
@Before
fun prepareLogger() {
mockkObject(NordicBlekLogger.Companion)
every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk()
}
@Test
fun addition_isCorrect() {
assertEquals(2, viewModel.test())
}
@Test
fun checkOnClick() = runTest {
every { viewModel.recordAccessControlPointCharacteristic } returns characteristic
coJustRun { characteristic.write(any(), any()) }
viewModel.onEvent(OnWorkingModeSelected(WorkingMode.FIRST))
advanceUntilIdle()
assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus)
}
@Test
fun `when connection fails return disconnected`() {
val serverDevice = mockk<ServerDevice>()
val disconnectedState = GattConnectionStateWithStatus(
GattConnectionState.STATE_DISCONNECTED,
BleGattConnectionStatus.SUCCESS
)
// every { viewModel.recordAccessControlPointCharacteristic } returns characteristic
// coJustRun { characteristic.write(any(), any()) }
mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientDeviceExtKt")
every { serverDevice.name } returns "Test"
every { serverDevice.address } returns "11:22:33:44:55"
every { stringConst.APP_NAME } returns "Test"
viewModel.handleResult(NavigationResult.Success(serverDevice))
assertEquals(disconnectedState, viewModel.state.value.glsServiceData.connectionState)
}
@Test
fun checkOnClick2() = runTest {
every { viewModel.recordAccessControlPointCharacteristic } returns characteristic
coJustRun { characteristic.write(any(), any()) }
mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientDeviceExtKt")
every { stringConst.APP_NAME } returns "Test"
justRun { viewModel.logAnalytics(any()) }
viewModel.handleResult(NavigationResult.Success(device))
viewModel.onEvent(OnWorkingModeSelected(WorkingMode.FIRST))
advanceUntilIdle()
assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus)
}
}

View File

@@ -86,7 +86,7 @@ class HRSRepository @Inject constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "HRS", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "HRS", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(HRSService::class.java, device)
}

View File

@@ -87,7 +87,7 @@ class HTSRepository @Inject constructor(
fun launch(device: ServerDevice) {
_data.value = _data.value.copy(deviceName = device.name)
logger = NordicBlekLogger(context, stringConst.APP_NAME, "HTS", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "HTS", device.address)
serviceManager.startService(HTSService::class.java, device)
}

View File

@@ -88,7 +88,7 @@ class PRXRepository @Inject internal constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "PRX", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "PRX", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(PRXService::class.java, device)
}

View File

@@ -85,7 +85,7 @@ class RSCSRepository @Inject constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "RSCS", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "RSCS", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(RSCSService::class.java, device)
}

View File

@@ -97,7 +97,7 @@ class UARTRepository @Inject internal constructor(
private fun shouldClean() = !isOnScreen && !isServiceRunning
fun launch(device: ServerDevice) {
logger = NordicBlekLogger(context, stringConst.APP_NAME, "UART", device.address)
logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "UART", device.address)
_data.value = _data.value.copy(deviceName = device.name)
serviceManager.startService(UARTService::class.java, device)
}