Skip to main content

Data Layer Tests

This page covers testing patterns for Repositories, Mappers, and DataStores. For coroutine test setup, mocking strategies, and Turbine flow testing patterns, see the Testing Strategy overview.

Repository Tests​

Repository tests mock the Retrofit API interfaces and mappers, then verify that the repository correctly orchestrates API calls, maps responses, and translates HTTP errors into domain exceptions.

class AuthRepositoryImplTest {

private lateinit var authApi: AuthApi
private lateinit var userMapper: UserMapper
private lateinit var repository: AuthRepositoryImpl

@Before
fun setup() {
authApi = mockk()
userMapper = mockk()
// ... other data stores mocked
repository = AuthRepositoryImpl(authApi, userMapper, /* ... */)
}

@Test
fun `login - returns LoginResult on success`() = runTest {
val responseDto = createLoginResponseDto()
val expectedResult = createLoginResult()
coEvery { authApi.login(any()) } returns responseDto
every { userMapper.toLoginResult(responseDto) } returns expectedResult

val result = repository.login("test@example.com", "password123")

assertEquals(expectedResult, result)
coVerify {
authApi.login(match { it.email == "test@example.com" && it.password == "password123" })
}
}

@Test
fun `login - throws InvalidCredentialsException on 401`() = runTest {
coEvery { authApi.login(any()) } throws createHttpException(HttpStatusCode.UNAUTHORIZED)

try {
repository.login("test@example.com", "wrong_password")
fail("Expected InvalidCredentialsException")
} catch (e: InvalidCredentialsException) {
assertNotNull(e)
}
}
}

Key patterns:

  • coEvery / coVerify for suspend function mocking.
  • match { } lambdas for argument assertions on request DTOs.
  • Retrofit HttpException created via okhttp3.ResponseBody.toResponseBody() for error path testing.
  • Domain exception types (NetworkException, InvalidCredentialsException, SessionExpiredException) are verified for each HTTP status code.

Mapper Tests​

Mapper tests are pure, with no mocking β€” they instantiate the mapper and assert on output:

class DeviceMapperTest {

private lateinit var mapper: DeviceMapper

@Before
fun setup() {
mapper = DeviceMapper()
}

@Test
fun `toDomain - complete dto - maps all fields correctly`() {
val dto = DeviceDto(
id = 42,
name = "My Car",
uniqueId = "ABC123",
status = "online",
// ... all fields
)

val result = mapper.toDomain(dto)

assertEquals(42, result.id)
assertEquals("My Car", result.name)
assertEquals(DeviceStatus.ONLINE, result.status)
// ... exhaustive field assertions
}

@Test
fun `toDomain - minimal dto - handles nulls gracefully`() {
val dto = DeviceDto(id = 1, name = "Device")

val result = mapper.toDomain(dto)

assertEquals(DeviceStatus.UNKNOWN, result.status)
assertNull(result.lastUpdate)
assertTrue(result.attributes.isEmpty())
}

@Test
fun `toDomain - status case insensitive - maps correctly`() {
val dto = DeviceDto(id = 1, name = "Device", status = "ONLINE")
assertEquals(DeviceStatus.ONLINE, mapper.toDomain(dto).status)
}
}

Mapper tests cover: complete mappings, null/missing field handling, status string parsing, edge cases (invalid timestamps, empty lists), and list mapping.

DataStore Tests​

DataStore tests use Turbine for Flow assertions. See the Flow Testing with Turbine section for the full pattern and DeviceDataStoreTest example.

DataStores expose reactive Flow properties that emit when data changes. Tests initialize the store, perform mutations, and use Turbine's .test { } block to assert on emitted values:

class DeviceDataStoreTest {

@Test
fun `updatePosition - emits to positionUpdates flow`() = runTest {
val device = createDeviceWithPosition(1)
dataStore.initialize(listOf(device))

val newPosition = createPosition(deviceId = 1, latitude = 45.0, longitude = -122.0)

dataStore.positionUpdates.test {
dataStore.updatePosition(newPosition)

val emitted = awaitItem()
assertEquals(45.0, emitted.latitude!!, 0.0001)
cancelAndIgnoreRemainingEvents()
}
}
}