Skip to content

Testing Your App & Quality Assurance ​

Learn how to write unit, UI, and automated CI/CD tests for your Android application when integrating the Veg Sahayyak SDK, simulate synthetic telematics streams, and verify our enterprise reliability guarantees.


🧪 1. Unit Testing Client ViewModels ​

The SDK provides the interface ISpeedRepository (com.vegsahayyak.speedcore.api.ISpeedRepository) to enable clean dependency injection and mocking in your test suites.

Testing with MockK & Turbine ​

When testing a ViewModel that consumes ISpeedRepository.observeSpeed(), use kotlinx-coroutines-test and Turbine to verify flow emissions:

kotlin
// DriverDashboardViewModel.kt
package com.example.fleetapp.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vegsahayyak.speedcore.api.ISpeedRepository
import com.vegsahayyak.speedcore.data.location.SpeedData
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

data class DriverUiState(
    val currentSpeedKmh: Float = 0f,
    val isOverSpeedAlert: Boolean = false,
    val isUnderSpeedAlert: Boolean = false,
    val isTracking: Boolean = false
)

class DriverDashboardViewModel(
    private val speedRepository: ISpeedRepository
) : ViewModel() {

    val uiState: StateFlow<DriverUiState> = speedRepository.observeSpeed()
        .map { data ->
            when (data) {
                is SpeedData.SpeedChanged -> DriverUiState(
                    currentSpeedKmh = data.speedMps * 3.6f,
                    isOverSpeedAlert = data.isOverLimit,
                    isUnderSpeedAlert = data.isUnderLimit,
                    isTracking = true
                )
                is SpeedData.LowAccuracy -> DriverUiState(isTracking = true)
                is SpeedData.SpeedUnavailable,
                is SpeedData.GPSDisabled,
                is SpeedData.Disabled -> DriverUiState(isTracking = false)
            }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), DriverUiState())
}
kotlin
// DriverDashboardViewModelTest.kt
package com.example.fleetapp.ui

import app.cash.turbine.test
import com.vegsahayyak.speedcore.api.ISpeedRepository
import com.vegsahayyak.speedcore.data.location.SpeedData
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

class DriverDashboardViewModelTest {

    private val speedRepository: ISpeedRepository = mockk()
    private val speedFlow = MutableStateFlow<SpeedData>(SpeedData.SpeedUnavailable)
    private lateinit var viewModel: DriverDashboardViewModel

    @Before
    fun setup() {
        every { speedRepository.observeSpeed() } returns speedFlow
        every { speedRepository.isEnable() } returns true
        viewModel = DriverDashboardViewModel(speedRepository)
    }

    @Test
    fun `when vehicle exceeds limit, uiState reflects over-speed alert`() = runTest {
        viewModel.uiState.test {
            // Initial state
            assertEquals(0f, awaitItem().currentSpeedKmh, 0.01f)

            // Emit over-speed reading (24.44 m/s ≈ 88.0 km/h > 80 km/h limit)
            speedFlow.value = SpeedData.SpeedChanged(
                speedMps = 24.44f,
                isOverLimit = true,
                isUnderLimit = false,
                accuracyMeters = 3.2f
            )

            val state = awaitItem()
            assertEquals(88.0f, state.currentSpeedKmh, 0.1f)
            assertTrue("Over-speed alert must be active", state.isOverSpeedAlert)
        }
    }
}

🛰️ 2. Synthetic Telematics Fixture (FakeSpeedRepository) ​

For automated UI tests or local emulator QA, use a FakeSpeedRepository. It eliminates emulator flakiness, runs instantaneously, and emits deterministic telematics events without GPS hardware:

kotlin
// Test Fixture: FakeSpeedRepository.kt
package com.example.fleetapp.testing

import com.vegsahayyak.speedcore.api.ISpeedRepository
import com.vegsahayyak.speedcore.data.location.SpeedData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class FakeSpeedRepository : ISpeedRepository {

    private val _speedFlow = MutableStateFlow<SpeedData>(SpeedData.SpeedUnavailable)
    private var isTrackingEnabled = true

    override fun observeSpeed(): Flow<SpeedData> = _speedFlow.asStateFlow()

    override fun isEnable(): Boolean = isTrackingEnabled
    override fun isLocationAvailable(): Boolean = _speedFlow.value !is SpeedData.GPSDisabled

    override fun toggle(boolean: Boolean) { isTrackingEnabled = boolean }
    override fun enable() { isTrackingEnabled = true }
    override fun disable() { isTrackingEnabled = false }

    /** Emit a simulated speed in km/h */
    fun emitSpeed(
        speedKmh: Float,
        accuracyMeters: Float = 3.0f,
        isOverLimit: Boolean = false,
        isUnderLimit: Boolean = false
    ) {
        _speedFlow.value = SpeedData.SpeedChanged(
            speedMps = speedKmh / 3.6f,
            isOverLimit = isOverLimit,
            isUnderLimit = isUnderLimit,
            accuracyMeters = accuracyMeters
        )
    }

    /** Simulate entering an active GPS dead-zone / tunnel */
    fun emitGpsLost() {
        _speedFlow.value = SpeedData.SpeedUnavailable
    }

    /** Simulate user disabling device location services */
    fun emitGpsDisabled() {
        _speedFlow.value = SpeedData.GPSDisabled
    }

    /** Simulate low accuracy signal jitter */
    fun emitLowAccuracy(reason: String = "GPS Accuracy: 35.0 m/s") {
        _speedFlow.value = SpeedData.LowAccuracy(reason)
    }
}

🎨 3. Jetpack Compose UI Testing ​

Test the Compose UI SpeedGauge and VoiceCommandButton components with the createComposeRule() test runner:

kotlin
// SpeedUiComposeTest.kt
package com.example.fleetapp.ui

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.vegsahayyak.speedui.components.SpeedGauge
import com.vegsahayyak.speedui.components.VoiceCommandButton
import com.vegsahayyak.speedcore.api.DrivingProfile
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SpeedUiComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun speedGaugeRendersSpeedAndAlertState() {
        composeTestRule.setContent {
            SpeedGauge(
                speed = 95f,
                warningSpeed = 80f,
                isTrackingActive = true,
                isOverLimit = true,
                unitLabel = "km/h"
            )
        }

        // Verify gauge renders updated numeric value and label
        composeTestRule.onNodeWithText("95").assertIsDisplayed()
        composeTestRule.onNodeWithText("km/h").assertIsDisplayed()
    }
    
    @Test
    fun voiceCommandButtonRenders() {
        composeTestRule.setContent {
            VoiceCommandButton(
                onCommandParsed = {}
            )
        }
        
        // Verify Voice Command button exists in hierarchy
        composeTestRule.onNodeWithContentDescription("Voice Command").assertIsDisplayed()
    }
}

💉 4. SDK Dependency Injection via SpeedAssistant.useRepository() ​

The SpeedAssistant SDK provides built-in repository substitution via useRepository(), allowing you to inject test doubles directly:

Direct Substitution in Integration Tests ​

kotlin
val fakeRepo = FakeSpeedRepository()

// Inject fake repository directly into SDK instance
val assistant = SpeedAssistant.with(context, "YOUR_JWT_LICENSE")
    .useRepository(fakeRepo)
    .setDrivingProfile(DrivingProfile.CarHighway)

assistant.start()

// Simulate a speed event
fakeRepo.emitSpeed(speedKmh = 85f, isOverLimit = true)

Hilt Test Module (@TestInstallIn) ​

If your app uses Hilt for dependency injection:

kotlin
// FakeSpeedRepositoryModule.kt
@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [SpeedAssistantModule::class]
)
object FakeSpeedRepositoryModule {

    @Provides
    @Singleton
    fun provideFakeSpeedRepository(): FakeSpeedRepository = FakeSpeedRepository()

    @Provides
    @Singleton
    fun provideSpeedRepository(fake: FakeSpeedRepository): ISpeedRepository = fake
}

📍 5. End-to-End GPS Mocking via FusedLocationProviderClient ​

For automated hardware-pipeline testing on physical devices or emulators, Google Play Services provides official mock location APIs via setMockMode and setMockLocation:

kotlin
// MockLocationHelper.kt
package com.example.fleetapp.testing

import android.annotation.SuppressLint
import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
import com.google.android.gms.location.FusedLocationProviderClient

class MockLocationHelper(private val fusedClient: FusedLocationProviderClient) {

    /** Enable mock mode (requires android.permission.ACCESS_MOCK_LOCATION in debug manifest) */
    @SuppressLint("MissingPermission")
    fun enableMockMode(enabled: Boolean) {
        fusedClient.setMockMode(enabled)
    }

    /** Inject a simulated GPS fix with explicit velocity & accuracy */
    @SuppressLint("MissingPermission")
    fun injectLocation(speedMps: Float, accuracyMeters: Float = 3.0f) {
        val mockLocation = Location(LocationManager.GPS_PROVIDER).apply {
            latitude = 12.9716
            longitude = 77.5946
            altitude = 920.0
            speed = speedMps
            accuracy = accuracyMeters
            time = System.currentTimeMillis()
            elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
        }
        fusedClient.setMockLocation(mockLocation)
    }
}

NOTE

setMockMode() requires the ACCESS_MOCK_LOCATION permission in your src/debug/AndroidManifest.xml and "Select mock location app" enabled in Android Developer Options on test devices.


📱 6. Robolectric Headless Service Testing ​

If you are verifying SDK initialization and preferences in JVM unit tests without an emulator, configure Robolectric:

kotlin
// SpeedAssistantServiceTest.kt
package com.example.fleetapp.service

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.vegsahayyak.speedcore.api.SpeedAssistant
import com.vegsahayyak.speedcore.api.DrivingProfile
import com.vegsahayyak.speedui.components.VoiceCommandParser
import com.vegsahayyak.speedui.api.VoiceAction
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class SpeedAssistantServiceTest {

    private lateinit var context: Context

    @Before
    fun setup() {
        context = ApplicationProvider.getApplicationContext()
    }

    @Test
    fun `with() initializes dependencies without throwing exceptions`() {
        val assistant = SpeedAssistant.with(context, "YOUR_JWT_LICENSE")
            .setDrivingProfile(DrivingProfile.CarHighway)

        assertNotNull(assistant.tracker)
        assertNotNull(assistant.preferences)
    }

    @Test
    fun `VoiceCommandParser parses profile correctly`() {
        val action = VoiceCommandParser.parse("set mode to highway")
        assertEquals(VoiceAction.SetProfile(DrivingProfile.CarHighway), action)
    }
}

🚀 7. Automated CI/CD Test Execution Commands ​

In continuous integration pipelines (GitHub Actions, GitLab CI, Bitrise), execute test suites with Gradle:

Test ScopeGradle CommandDescription
Unit & ViewModel Tests./gradlew testDebugUnitTestFast headless tests running on host JVM (MockK, Robolectric, Turbine).
Instrumentation & Compose UI./gradlew connectedDebugAndroidTestRuns automated UI tests on connected physical devices or CI emulators.
Lint & Deprecation Scan./gradlew lintDebugValidates Android API contracts, permissions, and layout integrity.

🛡️ 8. Enterprise Quality & Reliability Guarantees ​

The Veg Sahayyak SDK is built and verified against strict enterprise SLAs to ensure zero regressions in high-volume production apps:

Quality MetricSLA GuaranteeVerification Method
Mathematical Domain Coverage100% Branch CoverageAll EMA smoothing & Schmitt hysteresis formulas are tested against synthetic GPS jitter datasets.
Overall Line Coverage94% CoverageEnforced via automated JaCoCo gates on every pull request.
Memory & CPU Leak Audit0 Memory LeaksContinuous LeakCanary and Android Profiler checks during 24-hour endurance simulations.
ProGuard / R8 ObfuscationConsumer Rules ShippedPublic models (SpeedData, ISpeedRepository, SpeedUnit) are preserved; internal reflection is eliminated.
Android Version SupportAPI 23 – 37Verified across all Android OS versions and modern Foreground Service permission regimes.

🔒 9. ProGuard & R8 Configuration ​

The SDK bundles its own consumer-rules.pro inside speed-core.aar and speed-ui.aar. You do not need to add custom keep rules in your app's proguard-rules.pro:

properties
# Bundled automatically in speed-core.aar
-keep class com.vegsahayyak.speedcore.data.location.SpeedData** { *; }
-keep class com.vegsahayyak.speedcore.domain.TelematicsResult { *; }
-keep class com.vegsahayyak.speedcore.units.SpeedUnit { *; }
-keep class com.vegsahayyak.speedcore.api.DrivingProfile** { *; }
-keep class com.vegsahayyak.speedui.api.VoiceAction** { *; }
-keep interface com.vegsahayyak.speedcore.api.** { *; }

👉 Looking for internal SDK contribution guidelines? See our CI/CD & Release Pipeline in Operations & Standards.

Released under Commercial Enterprise License. Follow us on LinkedIn | Contact Us.