Learn Android integration testing from beginner to interview level: test boundaries, repository strategies, fake and real data sources, cache-first and network-first behaviour, offline support, synchronization, model/error mapping, Room DAO tests, database constraints and migration testing.
Sample Architecture Used in These Notes
flowchart LR
UI["ViewModel"]
REPO["TaskRepository"]
LOCAL["Local source<br/>Room DAO"]
REMOTE["Remote source<br/>API"]
DB["SQLite database"]
SERVER["Backend"]
UI --> REPO
REPO --> LOCAL --> DB
REPO --> REMOTE --> SERVERDomain model
data class Task(
val id: Long,
val title: String,
val isCompleted: Boolean
)Database entity
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey
val id: Long,
val title: String,
val isCompleted: Boolean,
val updatedAt: Long
)DAO
@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(task: TaskEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(tasks: List<TaskEntity>)
@Query("SELECT * FROM tasks ORDER BY title ASC")
fun observeTasks(): Flow<List<TaskEntity>>
@Query("SELECT * FROM tasks WHERE id = :id")
suspend fun getById(id: Long): TaskEntity?
@Update
suspend fun update(task: TaskEntity)
@Delete
suspend fun delete(task: TaskEntity)
@Query("DELETE FROM tasks")
suspend fun deleteAll()
}1. What Is Integration Testing?
Integration testing verifies that two or more components work correctly together.
Examples:
- Repository + fake API + fake DAO
- Repository + real Room database
- DAO + SQLite
- Retrofit + JSON converter
- Repository + mapper + cache
- Database migration + existing data
Example
A repository receives API data, maps it into entities, stores it in Room and returns domain models.
flowchart LR
API["API response"]
MAP1["Network mapper"]
REPO["Repository"]
DAO["Room DAO"]
MAP2["Database mapper"]
DOMAIN["Domain model"]
API --> MAP1 --> REPO
REPO --> DAO --> MAP2 --> DOMAINAn integration test checks that the connected components agree about:
- Data format
- Method contracts
- Error handling
- Ordering
- Persistence
- Synchronization
2. Unit Tests vs Integration Tests
| Point | Unit test | Integration test |
|---|---|---|
| Scope | One component | Multiple components |
| Dependencies | Replaced with doubles | Some dependencies are real |
| Speed | Very fast | Slower |
| Failure diagnosis | Easy | Moderate |
| Confidence | Logic confidence | Collaboration confidence |
| Android required | Usually no | Sometimes |
| Example | Mapper test | Repository + Room |
| Maintenance | Lower | Higher |
Unit-test example
Real repository
Fake local source
Fake remote sourceIntegration-test example
Real repository
Real Room database
Fake HTTP server
Real mappersUse unit tests for individual logic and integration tests for important boundaries between components.
3. Integration-Test Boundaries
A test boundary defines which components are real and which are replaced.
Repository integration boundary
flowchart LR
VM["ViewModel<br/>Excluded"]
REPO["Real repository"]
MAP["Real mappers"]
ROOM["Real Room database"]
API["Fake remote source"]
VM -.-> REPO
REPO --> MAP
REPO --> ROOM
REPO --> APIBefore writing the test, define:
- Subject under test
- Real collaborators
- Fake collaborators
- Inputs
- Observable result
- Failure being detected
Boundary examples
| Test | Real components | Replaced components |
|---|---|---|
| Repository unit test | Repository | API and DAO |
| Repository integration test | Repository and Room | Backend |
| DAO test | Room, DAO and SQLite | Nothing below DAO |
| API integration test | Retrofit and converter | Backend via fake server |
| E2E test | Most application layers | Few or none |
Do not make every dependency real. Use the smallest boundary that provides the required confidence.
\
4. Repository-Testing Strategy
A repository coordinates application data and hides data-source details from higher layers.
Test the repository’s observable contract:
- Which source provides reads?
- When is the network called?
- When is the cache updated?
- How are models mapped?
- How are errors mapped?
- What happens offline?
- How are conflicting values resolved?
- What is returned when data is stale?
- What happens during partial failure?
Recommended repository test matrix
| Scenario | Expected result |
|---|---|
| Cache contains fresh data | Return cache |
| Cache is empty | Fetch remote data |
| Remote request succeeds | Save and expose data |
| Remote request fails with cache | Return cache or stale state |
| Remote request fails without cache | Return error |
| Invalid remote model | Map to controlled error |
| Database write fails | Return persistence error |
| Local update pending | Preserve pending change |
| Sync succeeds | Mark data synchronized |
| Conflict occurs | Apply defined conflict policy |
Android architecture guidance recommends that each repository define a clear source of truth. Android data-layer guidance.
5. Repository Tests with Fake Data Sources
Fakes provide controlled data without using a real database or network.
Interfaces
interface TaskLocalDataSource {
fun observeTasks(): Flow<List<TaskEntity>>
suspend fun saveTasks(tasks: List<TaskEntity>)
}
interface TaskRemoteDataSource {
suspend fun fetchTasks(): List<TaskDto>
}Fake local source
class FakeTaskLocalDataSource : TaskLocalDataSource {
private val tasks =
MutableStateFlow<List<TaskEntity>>(emptyList())
var saveException: Exception? = null
override fun observeTasks(): Flow<List<TaskEntity>> {
return tasks
}
override suspend fun saveTasks(
tasks: List<TaskEntity>
) {
saveException?.let { throw it }
this.tasks.value = tasks
}
}Fake remote source
class FakeTaskRemoteDataSource(
var tasks: List<TaskDto> = emptyList(),
var exception: Exception? = null
) : TaskRemoteDataSource {
var requestCount = 0
override suspend fun fetchTasks(): List<TaskDto> {
requestCount++
exception?.let { throw it }
return tasks
}
}Repository test
@Test
fun refresh_remoteSucceeds_savesDataLocally() = runTest {
val local = FakeTaskLocalDataSource()
val remote = FakeTaskRemoteDataSource(
tasks = listOf(
TaskDto(1, "Integration testing", false)
)
)
val repository = TaskRepositoryImpl(
local = local,
remote = remote
)
repository.refresh()
val result = local.observeTasks().first()
assertEquals(1, result.size)
assertEquals("Integration testing", result.first().title)
}Fake data sources make repository tests fast and deterministic. Test doubles are specifically useful for controlling dependency behaviour. Android test-double guidance.
6. Repository Tests with Real Components
Use real components when you need confidence that actual integration works.
Possible setup:
Real TaskRepositoryImpl
Real mappers
Real Room database
Fake remote APIExample setup
@RunWith(AndroidJUnit4::class)
class TaskRepositoryIntegrationTest {
private lateinit var database: AppDatabase
private lateinit var repository: TaskRepository
@Before
fun setUp() {
val context =
ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
).allowMainThreadQueries().build()
repository = TaskRepositoryImpl(
dao = database.taskDao(),
remote = FakeTaskRemoteDataSource()
)
}
@After
fun tearDown() {
database.close()
}
}This can detect:
- Incorrect SQL
- Wrong entity mapping
- Conflict-strategy problems
- Flow update problems
- Data not being persisted
- Incorrect transaction behaviour
Room tests commonly run as instrumented tests. Robolectric can be considered when JVM-based Android simulation fits the project.
7. Local and Remote Data Coordination
A repository often coordinates:
- Local database
- Remote API
- Cache freshness
- Pending local changes
- Sync metadata
Single source of truth pattern
flowchart TD
UI["UI observes data"]
DB["Room database<br/>Source of truth"]
REPO["Repository"]
API["Remote API"]
DB --> UI
REPO --> API
API --> REPO
REPO --> DBRecommended flow:
1. UI observes Room.
2. Repository fetches remote data.
3. Repository writes remote data into Room.
4. Room emits updated data.
5. UI receives the update.
This avoids returning one value from the network while exposing a different value from the database.
In an offline-first architecture, Android recommends using a local source as the canonical source of truth and synchronizing it with the network. Offline-first architecture.
Test
@Test
fun refresh_updatesObservedLocalData() = runTest {
repository.observeTasks().test {
assertEquals(emptyList<Task>(), awaitItem())
repository.refresh()
assertEquals(expectedTasks, awaitItem())
}
}8. Cache-First Behaviour
Cache-first means the repository reads cached data before requesting remote data.
Typical decision
flowchart TD
R["Request data"]
C{"Fresh cache exists?"}
LC["Return local cache"]
N["Fetch network"]
S["Save locally"]
O["Return/observe data"]
R --> C
C -->|Yes| LC
C -->|No| N
N --> S --> OImportant tests
1. Fresh cache is returned.
2. Network is not called for fresh cache.
3. Empty cache triggers network.
4. Expired cache triggers network.
5. Network success updates cache.
6. Network failure returns stale cache if policy permits.
7. Network failure without cache returns error.
@Test
fun freshCacheExists_networkIsNotCalled() = runTest {
local.tasks = cachedTasks
local.lastUpdated = 10_000
clock.currentTime = 11_000
val result = repository.getTasks()
assertEquals(cachedTasks, result)
assertEquals(0, remote.requestCount)
}Inject a clock so cache-expiry tests do not use real time.
9. Network-First Behaviour
Network-first tries the remote source before local data.
flowchart TD
R["Request data"]
N["Fetch remote"]
OK{"Successful?"}
S["Save in Room"]
L["Read local fallback"]
E["Return error"]
R --> N --> OK
OK -->|Yes| S
OK -->|No, cache exists| L
OK -->|No cache| EImportant tests
- Successful network response returned
- Successful response cached
- Network failure falls back to cache
- Network failure without cache returns error
- Malformed response is not stored
- Existing cache is preserved after network failure
@Test
fun networkFails_cachedDataIsReturned() = runTest {
local.tasks = cachedTasks
remote.exception = IOException()
val result = repository.getTasks()
assertEquals(cachedTasks, result)
}Network-first is useful when freshness matters more than immediate response speed.
10. Offline Behaviour
Offline behaviour defines what the app can do without connectivity.
An offline-first app should, at minimum, support critical reads without network access. Android offline-first guidance.
Offline scenarios
| Scenario | Expected behaviour |
|---|---|
| Network unavailable, cache exists | Show cached data |
| Network unavailable, cache empty | Show offline error |
| User creates data offline | Save as pending |
| Connectivity returns | Synchronize pending data |
| Sync fails | Keep pending data |
| Server rejects update | Mark conflict/error |
| App restarts | Pending operation remains |
Pending entity example
@Entity
data class PendingTaskEntity(
@PrimaryKey
val localId: String,
val title: String,
val syncStatus: String
)Possible statuses:
enum class SyncStatus {
PENDING,
SYNCING,
SYNCED,
FAILED
}Test
@Test
fun createTaskOffline_isStoredAsPending() = runTest {
remote.exception = IOException()
repository.createTask("Offline task")
val stored = pendingTaskDao.getAll()
assertEquals(1, stored.size)
assertEquals(
SyncStatus.PENDING,
stored.first().syncStatus
)
}11. Data Synchronization
Synchronization keeps local and remote data consistent.
Synchronization flow
flowchart TD
P["Read pending local changes"]
U["Upload to server"]
OK{"Upload successful?"}
M["Merge server response"]
S["Mark synchronized"]
F["Keep pending/failed"]
P --> U --> OK
OK -->|Yes| M --> S
OK -->|No| FTest cases
- Only pending records are uploaded
- Successful items become synchronized
- Failed items remain pending
- Duplicate uploads are prevented
- Sync can resume after interruption
- Deletion synchronizes correctly
- Partial success is handled
- Conflicts use the defined strategy
- Multiple sync runs remain idempotent
Common conflict policies
| Policy | Meaning |
|---|---|
| Server wins | Remote version replaces local |
| Client wins | Local version replaces remote |
| Last write wins | Latest timestamp wins |
| Field-level merge | Individual fields are merged |
| Manual resolution | User chooses the correct value |
Do not rely only on device time for critical conflict resolution because device clocks may be incorrect.
12. Domain-Model Mapping
Different layers often use different models:
TaskDto → Network representation
TaskEntity → Database representation
Task → Domain representation
TaskUiModel → UI representation
flowchart LR
DTO["TaskDto"]
ENTITY["TaskEntity"]
DOMAIN["Task"]
UI["TaskUiModel"]
DTO --> ENTITY --> DOMAIN --> UIMapper
fun TaskEntity.toDomain(): Task {
return Task(
id = id,
title = title,
isCompleted = isCompleted
)
}Test
@Test
fun entity_mapsToDomainModel() {
val entity = TaskEntity(
id = 1,
title = "Room testing",
isCompleted = false,
updatedAt = 1_000
)
val result = entity.toDomain()
assertEquals(
Task(
id = 1,
title = "Room testing",
isCompleted = false
),
result
)
}Test:
- Every field
- Null handling
- Default values
- Enum conversion
- Date conversion
- Unknown server values
- Precision
- Invalid identifiers
Mapper unit tests are useful, but repository integration tests should also verify that mappers are correctly connected.
13. API Error Mapping
The data layer should convert technical failures into stable application errors.
sealed interface DataError {
data object NetworkUnavailable : DataError
data object Timeout : DataError
data object Unauthorized : DataError
data object NotFound : DataError
data object Server : DataError
data object InvalidResponse : DataError
data object Unknown : DataError
}Mapping example
fun Throwable.toDataError(): DataError {
return when (this) {
is UnknownHostException ->
DataError.NetworkUnavailable
is SocketTimeoutException ->
DataError.Timeout
is JsonDataException ->
DataError.InvalidResponse
else ->
DataError.Unknown
}
}HTTP status mapping might include:
| Failure | Domain error |
|---|---|
| No internet | NetworkUnavailable |
| Timeout | Timeout |
| HTTP 401 | Unauthorized |
| HTTP 404 | NotFound |
| HTTP 5xx | Server |
| Invalid JSON | InvalidResponse |
Test
@Test
fun timeoutException_mapsToTimeoutError() {
val error =
SocketTimeoutException().toDataError()
assertEquals(
DataError.Timeout,
error
)
}Repository tests should confirm that technical exceptions do not escape unexpectedly.
14. Database Error Mapping
Database operations can fail because of:
- Constraint violations
- Full storage
- Corrupt database
- Closed database
- Failed migration
- Invalid SQL
- Disk I/O failure
Map infrastructure exceptions into stable data-layer errors.
sealed interface StorageError {
data object ConstraintViolation : StorageError
data object StorageUnavailable : StorageError
data object CorruptedData : StorageError
data object Unknown : StorageError
}Test
@Test
fun localSaveFails_repositoryReturnsStorageError() = runTest {
local.saveException =
SQLiteConstraintException()
val result = repository.save(expectedTask)
assertEquals(
SaveResult.Failure(
StorageError.ConstraintViolation
),
result
)
}Avoid showing raw database error messages to users.
15. Room Database Testing
Room testing verifies:
- DAO queries
- Inserts
- Updates
- Deletes
- Relationships
- Constraints
- Conflict strategies
- Transactions
- Flow emissions
- Migrations
Room tests are important because SQL queries are not fully verified by ordinary mock-based tests.
Android’s Room guidance emphasizes testing database stability and protecting user data. Room database testing.
Recommended setup
@RunWith(AndroidJUnit4::class)
class TaskDaoTest {
private lateinit var database: AppDatabase
private lateinit var dao: TaskDao
@Before
fun createDatabase() {
val context =
ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
).allowMainThreadQueries().build()
dao = database.taskDao()
}
@After
fun closeDatabase() {
database.close()
}
}Place instrumented Room tests under:
app/src/androidTest/java/16. In-Memory Room Database
An in-memory database stores data in memory rather than a persistent file.
database = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
).build()Advantages
- Fresh database for every test
- Fast
- No persistent test data
- Test isolation
- Uses real Room and SQLite behaviour
Limitations
- Data disappears when the database closes
- Does not simulate all disk failures
- Cannot replace migration testing
- Still requires proper setup and cleanup
allowMainThreadQueries() is acceptable in tightly controlled tests, but should generally not be enabled in production.
17. DAO Insert Testing
Test:
- Single insert
- Multiple inserts
- Generated ID
- Duplicate primary key
- Conflict strategy
- Nullability and defaults
@Test
fun insertTask_taskCanBeRead() = runTest {
val task = TaskEntity(
id = 1,
title = "Learn Room",
isCompleted = false,
updatedAt = 1_000
)
dao.insert(task)
assertEquals(
task,
dao.getById(1)
)
}Conflict-strategy test
@Test
fun insertSameId_replaceStrategyUpdatesTask() = runTest {
dao.insert(
createTask(id = 1, title = "Old")
)
dao.insert(
createTask(id = 1, title = "New")
)
assertEquals(
"New",
dao.getById(1)?.title
)
}The expected result depends on whether the DAO uses REPLACE, IGNORE, ABORT or another strategy.
18. DAO Read Testing
Test:
- Read existing record
- Read missing record
- Empty table
- Filtering
- Flow observation
- Relations
- Nullable fields
@Test
fun getById_unknownId_returnsNull() = runTest {
val result = dao.getById(999)
assertNull(result)
}Testing Room Flow
@Test
fun observeTasks_afterInsert_emitsUpdatedList() = runTest {
dao.observeTasks().test {
assertEquals(
emptyList<TaskEntity>(),
awaitItem()
)
val task = createTask(
id = 1,
title = "Integration testing"
)
dao.insert(task)
assertEquals(
listOf(task),
awaitItem()
)
}
}Room observable queries normally emit again when a relevant table changes.
19. DAO Update and Delete Testing
Update test
@Test
fun updateTask_changesStoredRecord() = runTest {
val original = createTask(
id = 1,
title = "Old title"
)
dao.insert(original)
val updated = original.copy(
title = "Updated title",
isCompleted = true
)
dao.update(updated)
assertEquals(
updated,
dao.getById(1)
)
}Delete test
@Test
fun deleteTask_removesRecord() = runTest {
val task = createTask(id = 1)
dao.insert(task)
dao.delete(task)
assertNull(dao.getById(1))
}Also test:
- Updating a missing record
- Deleting a missing record
- Delete-all operation
- Cascading delete
- Transaction rollback
Where meaningful, return affected row counts from DAO methods and assert them.
20. Query and Ordering Tests
Queries can fail logically even if they compile.
Test:
- Ascending order
- Descending order
- Filtering
- Search
- Pagination
- Case sensitivity
- Null ordering
- Duplicate values
- Date range
- Empty result
DAO query
@Query(
"""
SELECT * FROM tasks
WHERE isCompleted = :completed
ORDER BY title ASC
"""
)
suspend fun getTasksByStatus(
completed: Boolean
): List<TaskEntity>Test
@Test
fun completedTasks_areSortedByTitle() = runTest {
dao.insertAll(
listOf(
createTask(1, "Zebra", true),
createTask(2, "Android", true),
createTask(3, "Kotlin", false)
)
)
val result =
dao.getTasksByStatus(completed = true)
assertEquals(
listOf("Android", "Zebra"),
result.map { it.title }
)
}Never assume database ordering unless the SQL query contains ORDER BY.
21. Database Constraint Testing
Database constraints protect data integrity.
Common constraints:
- Primary key
- Unique index
- Foreign key
- Non-null column
- Check constraint where supported
- Referential actions such as cascade delete
Unique-index example
@Entity(
tableName = "users",
indices = [
Index(
value = ["email"],
unique = true
)
]
)
data class UserEntity(
@PrimaryKey val id: Long,
val email: String
)Test
@Test
fun duplicateEmail_throwsConstraintException() = runTest {
dao.insertUser(
UserEntity(1, "user@example.com")
)
assertThrows(
SQLiteConstraintException::class.java
) {
runTest {
dao.insertUser(
UserEntity(2, "user@example.com")
)
}
}
}Foreign-key scenarios
Test:
- Child with valid parent
- Child without parent
- Parent deletion with
CASCADE - Parent deletion with
RESTRICT - Parent-key update behaviour
Constraints should be tested using real Room/SQLite, not mocks.
22. Room Migration Testing
A migration changes the database schema while preserving existing user data.
Examples:
- Add column
- Rename table
- Create index
- Split one table into two
- Change relationship
- Transform existing values
Why migration tests are critical
A migration may work for a fresh installation but fail for an existing user upgrading from an older version.
flowchart LR
V1["Database v1<br/>Existing data"]
M["Migration 1 → 2"]
V2["Database v2<br/>New schema"]
C["Verify schema and data"]
V1 --> M --> V2 --> CExport schemas
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
"room.schemaLocation" to
"$projectDir/schemas"
)
}
}
}
}For KSP-based Room configuration, configure the equivalent Room schema directory according to the project’s Room plugin/KSP setup.
Commit exported schema files to version control.
Migration
val MIGRATION_1_2 =
object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
ALTER TABLE tasks
ADD COLUMN priority INTEGER NOT NULL DEFAULT 0
""".trimIndent()
)
}
}Migration test
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry
.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate1To2_preservesTasksAndAddsPriority() {
helper.createDatabase(TEST_DB, 1).apply {
execSQL(
"""
INSERT INTO tasks
(id, title, isCompleted, updatedAt)
VALUES (1, 'Existing task', 0, 1000)
""".trimIndent()
)
close()
}
helper.runMigrationsAndValidate(
TEST_DB,
2,
true,
MIGRATION_1_2
).use { database ->
database.query(
"SELECT title, priority FROM tasks WHERE id = 1"
).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(
"Existing task",
cursor.getString(0)
)
assertEquals(0, cursor.getInt(1))
}
}
}MigrationTestHelper uses exported schemas to create old database versions and validate migrated schemas. Room migration testing.
Test every supported migration path
If the current version is 4, consider testing:
1 → 2
2 → 3
3 → 4
1 → 4
2 → 4Also test:
- Existing rows preserved
- New columns receive correct defaults
- Indexes created
- Relationships remain valid
- Data transformations are correct
- Complete migration path succeeds
Avoid fallbackToDestructiveMigration() when user data must be preserved.
Complete Repository Use Case
Requirement
The task list should:
1. Display Room data immediately.
2. Refresh from the server.
3. Save the server response into Room.
4. Continue displaying cached data offline.
5. Preserve locally created pending tasks.
6. Synchronize pending tasks when connectivity returns.
Test matrix
| Scenario | Components | Expected result |
|---|---|---|
| Initial local data | Repository + Room | Cached data emitted |
| Refresh success | Repository + Room + fake remote | Database updated |
| Refresh failure | Repository + Room + failing remote | Cache preserved |
| No cache and no network | Repository + Room | Offline error |
| Local task creation | Repository + Room | Pending record saved |
| Successful synchronization | All except real server | Record marked synced |
| Partial synchronization | Repository + Room | Failed records remain pending |
| Server/local conflict | Repository + policy | Defined winner/merge |
| Upgrade old database | Migration helper | Data and schema preserved |
Common Mistakes
1. Calling a repository test an integration test when every dependency is mocked
2. Making the entire application real in one test
3. Using a production backend
4. Sharing one database between tests
5. Forgetting to close the database
6. Depending on test execution order
7. Returning network data without updating the source of truth
8. Deleting valid cache after a network failure
9. Testing only the online path
10. Ignoring partial synchronization
11. Using device time as the only conflict authority
12. Exposing DTOs or entities as domain models
13. Leaking Retrofit or SQLite exceptions to the UI
14. Assuming query ordering without ORDER BY15. Testing DAO behaviour with mocks
16. Not testing duplicate primary keys
17. Ignoring foreign-key behaviour
18. Using destructive migration for valuable user data
19. Testing only fresh database creation
20. Not committing exported Room schemas
21. Forgetting older-to-latest migration paths
22. Using allowMainThreadQueries() in production
23. Using arbitrary delays for Room Flow tests
24. Failing to test empty database results
25. Clearing pending offline changes during refresh
Interview Questions and Answers
1. What is integration testing?
Integration testing verifies that multiple components communicate and behave correctly together.
2. Unit test vs integration test?
A unit test isolates one component. An integration test connects multiple real components across a meaningful boundary.
3. Give an Android integration-test example.
Testing a real repository with a real Room database and a fake remote data source.
4. What is an integration-test boundary?
It defines which components are real, which are replaced and which behaviour the test verifies.
5. Why should the boundary remain small?
A smaller boundary makes tests faster and failures easier to diagnose.
6. What should be tested in a repository?
Source selection, mapping, caching, synchronization, fallback behaviour and error handling.
7. How do you test a repository with fake data sources?
Provide configurable fake local and remote implementations, execute the repository operation and assert outputs and stored state.
8. What is the benefit of fake data sources?
They are fast, deterministic and can simulate success, failure and different data states.
9. Why test a repository with real Room?
It detects SQL, mapping, persistence, transaction and Flow-notification problems that fakes may miss.
10. Should a repository integration test use the production server?
Normally no. Use a fake server or fake remote source to keep tests deterministic.
11. What is a single source of truth?
It is the authoritative source from which higher layers read consistent application data.
12. What is commonly the source of truth in an offline-first app?
A local persistent source such as Room.
13. How should network data reach the UI in an offline-first design?
The repository stores it locally, and the UI observes the local source.
14. What is cache-first behaviour?
The repository uses fresh cached data first and fetches the network when the cache is missing or stale.
15. What is network-first behaviour?
The repository requests current remote data first and falls back to local data when allowed.
16. Cache-first vs network-first?
Cache-first prioritizes speed and offline availability. Network-first prioritizes freshness.
17. What should happen when the network fails but cache exists?
According to the product policy, return or continue showing cached data, usually with a stale/offline indicator.
18. What should happen when both network and cache are unavailable?
Return a controlled offline or unavailable-data error.
19. What is offline-first architecture?
It makes critical functionality available from local data and synchronizes local and remote state when connectivity permits.
20. How are offline writes handled?
Store them locally with a pending status and upload them later.
21. What is data synchronization?
It reconciles local and remote data while preserving consistency and tracking pending operations.
22. What are common synchronization conflict strategies?
Server wins, client wins, last write wins, field-level merge and manual resolution.
23. What is idempotent synchronization?
Repeating the same sync operation does not create an incorrect additional effect or duplicate record.
24. Why use separate DTO, entity and domain models?
Each layer has different requirements, and separation prevents network or database details from leaking across architecture layers.
25. What should a mapper test verify?
Field mapping, null handling, defaults, types, formatting and unknown values.
26. How should API errors be handled?
Convert HTTP, connectivity, timeout and parsing failures into stable data/domain errors.
27. How should database errors be handled?
Map storage and constraint exceptions into meaningful application-level errors without exposing raw messages.
28. Why test Room DAOs with a real database?
DAO behaviour depends on generated Room code and SQLite semantics, which mocks cannot verify.
29. What is an in-memory Room database?
A temporary Room database stored in memory and discarded when closed.
30. Why use an in-memory database in tests?
It provides isolation, speed and real Room/SQLite behaviour without persistent test files.
31. What is the limitation of an in-memory database?
It does not test real upgrade files or every disk-related failure and cannot replace migration tests.
32. What should be tested for DAO inserts?
Inserted data, generated IDs, conflict strategies, defaults and constraint failures.
33. What should be tested for DAO reads?
Existing, missing and empty results, filters, relations and observable updates.
34. What should be tested for updates?
The correct row and fields change while unrelated data remains unchanged.
35. What should be tested for deletes?
The row is removed and foreign-key cascade or restriction behaviour is correct.
36. Is database result order guaranteed?
No. Use ORDER BY when ordering is required.
37. What database constraints should be tested?
Primary keys, unique indexes, foreign keys, nullability and referential actions.
38. What is Room migration testing?
It verifies that an older schema upgrades to a newer schema without losing or corrupting user data.
39. What is MigrationTestHelper?
It creates databases at old schema versions, applies migrations and validates the resulting schema.
40. Why export Room schemas?
Migration tests use them for schema creation and validation, and they provide versioned schema history.
41. Should Room schema files be committed?
Yes. They should normally be version-controlled.
42. Why test older-to-latest migration paths?
Real users may upgrade from several older application versions, not only the immediately previous version.
43. What is destructive migration?
It recreates the database when a valid migration path is unavailable, causing existing local data to be lost.
44. When is destructive migration dangerous?
When the database contains user-created, business-critical or offline-pending data.
Quick Revision
Unit test:
One component + controlled dependencies
Integration test:
Multiple real collaborating components
Repository test:
Source selection
Mapping
Caching
Fallback
Synchronization
Error handling
Cache first:
Local → network when stale/missing
Network first:
Network → local fallback
Offline first:
Room is commonly the source of truth
UI reads Room
Network synchronizes Room
Room tests:
Insert
Read
Update
Delete
Query/order
Constraints
Flow emissions
Migrations
Migration test:
Old schema + existing data
→ Apply migration
→ Validate new schema
→ Verify data preserved