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:
// 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())
}// 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:
// 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:
// 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
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:
// 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:
// 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:
// 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 Scope | Gradle Command | Description |
|---|---|---|
| Unit & ViewModel Tests | ./gradlew testDebugUnitTest | Fast headless tests running on host JVM (MockK, Robolectric, Turbine). |
| Instrumentation & Compose UI | ./gradlew connectedDebugAndroidTest | Runs automated UI tests on connected physical devices or CI emulators. |
| Lint & Deprecation Scan | ./gradlew lintDebug | Validates 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 Metric | SLA Guarantee | Verification Method |
|---|---|---|
| Mathematical Domain Coverage | 100% Branch Coverage | All EMA smoothing & Schmitt hysteresis formulas are tested against synthetic GPS jitter datasets. |
| Overall Line Coverage | 94% Coverage | Enforced via automated JaCoCo gates on every pull request. |
| Memory & CPU Leak Audit | 0 Memory Leaks | Continuous LeakCanary and Android Profiler checks during 24-hour endurance simulations. |
| ProGuard / R8 Obfuscation | Consumer Rules Shipped | Public models (SpeedData, ISpeedRepository, SpeedUnit) are preserved; internal reflection is eliminated. |
| Android Version Support | API 23 – 37 | Verified 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:
# 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.