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/coVerifyfor suspend function mocking.match { }lambdas for argument assertions on request DTOs.- Retrofit
HttpExceptioncreated viaokhttp3.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()
}
}
}