Room/Repository/Network Testing: Mocks vs Fakes
Test your data layer with confidence — in-memory databases, MockWebServer, and fake repositories
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Data layer testing covers DAOs (with in-memory Room), network clients (with MockWebServer), and repositories (with fakes or mocks). In-memory Room databases provide a real SQLite engine with zero setup and instant reset. MockWebServer provides a real HTTP server on localhost, making Retrofit tests fully deterministic without network access. Fake repositories simulate real caching and state behavior without a database. Together these tools let you test your entire data layer — including offline-first sync, error handling, and JSON contracts — reliably and fast.
Real-world relevance
An enterprise field operations app had an offline-first sync that was breaking intermittently in production: sometimes the app showed stale data after syncing. The bug was in the repository: it updated the in-memory cache before saving to the local database, so if the app crashed mid-sync, the cache showed new data but Room still had old data. An integration test with in-memory Room + MockWebServer caught this by crashing the repository mid-sync and verifying that the database and cache were consistent. The fix — writing to Room first, then updating the cache from the Room Flow — was verified by the test and eliminated the intermittent stale data reports.
Key points
- Testing Room with in-memory database — Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build() creates a fully functional Room database backed by SQLite in memory — no files, resets on close. Use @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() for LiveData and provide a test coroutine context. In-memory DB is faster than disk and perfectly isolates tests.
- DAO unit tests — the right approach — Test DAOs directly against the in-memory database: insert a record, query it, assert correctness. Test insert+query, update+query, delete+query cycles. Test Flow emissions from DAO queries: insert data, collect the Flow, assert the emission. These are technically instrumentation tests (need Context) but run fast on Robolectric.
- MockWebServer for Retrofit testing — OkHttp's MockWebServer lets you run a real HTTP server locally in tests. enqueue(MockResponse().setBody(jsonString)) pre-loads responses. Your Retrofit client makes real HTTP calls to localhost:PORT. Assert on the request (path, headers, body) AND the response parsing. No network needed, fully deterministic.
- Testing JSON parsing and API contracts — MockWebServer tests verify two things: (1) your Retrofit/Moshi/Gson correctly parses the API response JSON into your data models, and (2) your API service sends the correct request (right path, right headers, right body). This catches contract violations before they reach production.
- Fake vs Mock repository — when to use each — Use a Fake repository (real in-memory implementation) when: the repository has complex state that matters to the test (e.g., pagination, caching). Use a Mock repository when: you only need the repository to return a specific value and do not care about its internal state. Fakes make tests more realistic; Mocks are faster to set up.
- Testing offline-first sync logic — An offline-first repository fetches from the local database first, then updates from the network and saves to the local database. Test this by: (1) pre-populate the fake local DB, (2) enqueue a MockWebServer response, (3) call the repository, (4) assert the Flow emitted first from local DB, then updated with network data. This sequence tests the entire offline-first contract.
- Integration test patterns — @MediumTest — Integration tests that test multiple layers together (e.g., DAO + Repository) are annotated with @MediumTest (conceptual categorization). They verify the wiring between components. Keep them focused: test one integration point per test, not the entire app stack. Use Room in-memory + MockWebServer for the most realistic integration tests.
- Testing error scenarios at the data layer — Test: network timeout (MockWebServer sets no response, OkHttp times out), HTTP 4xx/5xx responses (MockWebServer.enqueue(MockResponse().setResponseCode(500))), malformed JSON (causes parsing exception), database constraint violations (duplicate primary key insert). Error tests at the data layer prevent cascading failures in ViewModels and use cases.
- Test isolation — each test is independent — Every test should start with a clean state: close and recreate the in-memory database in @BeforeEach, use a new MockWebServer instance per test (or clear enqueued responses), clear the fake repository's storage. Non-isolated tests produce order-dependent failures — the most mysterious kind of test failure.
- Testing Room migrations — Room migration tests verify that your database schema can migrate from version N to N+1 without losing data. Use MigrationTestHelper from androidx.room:room-testing. Create the DB at version N, insert test data, run the migration, verify the data is still present and schema is correct. Critical for apps that have been in production — a bad migration deletes user data.
- Verifying request content with MockWebServer — val request = server.takeRequest() gives you the RecordedRequest — check request.path, request.method, request.body.readUtf8() to verify your API client sends the correct payload. For a fintech transfer API, verify the exact JSON body including amount precision and currency code — wrong request format = rejected transaction.
- Using TestCoroutineDispatcher with Room and Retrofit — Room Flow emissions and Retrofit calls are async. Wrap in runTest, use advanceUntilIdle() for Room queries that return Flow, and use standard await patterns for one-shot suspend functions. Room with in-memory + coroutines works cleanly in runTest as long as the database is created with allowMainThreadQueries() for simple tests.
Code example
// In-memory Room database test (instrumentation test with Robolectric or emulator)
@RunWith(AndroidJUnit4::class)
class WorkOrderDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: WorkOrderDao
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries() // OK for tests only
.build()
dao = db.workOrderDao()
}
@After
fun tearDown() {
db.close() // releases in-memory database, resets all state
}
@Test
fun insertAndQueryWorkOrder() = runTest {
val order = WorkOrder(id = "w1", title = "Fix Elevator", status = "OPEN")
dao.insert(order)
val retrieved = dao.getById("w1")
assertThat(retrieved?.title).isEqualTo("Fix Elevator")
assertThat(retrieved?.status).isEqualTo("OPEN")
}
@Test
fun updateWorkOrderStatus() = runTest {
dao.insert(WorkOrder(id = "w1", title = "Fix Elevator", status = "OPEN"))
dao.updateStatus("w1", "COMPLETED")
assertThat(dao.getById("w1")?.status).isEqualTo("COMPLETED")
}
@Test
fun workOrderFlow_emitsOnInsert() = runTest {
dao.getAllOrdersFlow().test {
assertThat(awaitItem()).isEmpty() // initial empty state
dao.insert(WorkOrder("w1", "Fix Elevator", "OPEN"))
assertThat(awaitItem()).hasSize(1) // emits after insert
dao.insert(WorkOrder("w2", "Replace HVAC", "OPEN"))
assertThat(awaitItem()).hasSize(2) // emits after second insert
cancel()
}
}
}
// MockWebServer for Retrofit testing
class WorkOrderApiTest {
private lateinit var server: MockWebServer
private lateinit var api: WorkOrderApi
@Before
fun setUp() {
server = MockWebServer()
server.start()
api = Retrofit.Builder()
.baseUrl(server.url("/")) // points to local MockWebServer
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(WorkOrderApi::class.java)
}
@After
fun tearDown() {
server.shutdown()
}
@Test
fun `fetchOrders parses response correctly`() = runTest {
val json = """
[{"id":"w1","title":"Fix Elevator","status":"OPEN"},
{"id":"w2","title":"Replace HVAC","status":"OPEN"}]
""".trimIndent()
server.enqueue(MockResponse().setBody(json).setResponseCode(200))
val orders = api.fetchOrders()
assertThat(orders).hasSize(2)
assertThat(orders[0].id).isEqualTo("w1")
assertThat(orders[1].title).isEqualTo("Replace HVAC")
// Verify the request was correct
val request = server.takeRequest()
assertThat(request.path).isEqualTo("/orders")
assertThat(request.method).isEqualTo("GET")
}
@Test
fun `fetchOrders throws on 500 response`() = runTest {
server.enqueue(MockResponse().setResponseCode(500))
assertThrows<HttpException> {
api.fetchOrders()
}
}
@Test
fun `createOrder sends correct request body`() = runTest {
server.enqueue(MockResponse().setBody("""{"id":"w3","title":"New Order","status":"OPEN"}"""))
api.createOrder(CreateOrderRequest(title = "New Order", priority = "HIGH"))
val request = server.takeRequest()
assertThat(request.method).isEqualTo("POST")
assertThat(request.path).isEqualTo("/orders")
val body = request.body.readUtf8()
assertThat(body).contains(""title":"New Order"")
assertThat(body).contains(""priority":"HIGH"")
}
}Line-by-line walkthrough
- 1. Room.inMemoryDatabaseBuilder() creates a real SQLite database in RAM — all Room features (queries, transactions, Flow) work identically to a file database.
- 2. allowMainThreadQueries() is acceptable in tests — it removes the Room restriction that prevents running queries on the main thread, simplifying test code.
- 3. db.close() in @After is critical — it releases the in-memory SQLite engine and discards all data, ensuring the next test starts completely fresh.
- 4. dao.getAllOrdersFlow().test { } from Turbine collects Flow emissions from the DAO — Room automatically emits a new list every time the underlying table changes.
- 5. awaitItem() after dao.insert() works because Room's Flow support watches the table and emits on every write — Turbine suspends until that emission arrives.
- 6. MockWebServer.start() binds to a random available port on localhost — server.url('/') returns the base URL including the port, which you pass to Retrofit.
- 7. server.enqueue() pre-loads responses in a FIFO queue — the first HTTP request gets the first enqueued response, the second request gets the second, and so on.
- 8. server.takeRequest() dequeues the RecordedRequest for the last HTTP call — use it after making the API call to inspect what your Retrofit client actually sent.
- 9. assertThat(request.body.readUtf8()).contains() verifies the JSON request body — catches bugs where a field is missing or has the wrong key name.
- 10. server.shutdown() in @After stops the local HTTP server — failing to shut down leaks server threads, causing test suites to hang or subsequent tests to fail with port conflicts.
Spot the bug
class StudentRepositoryIntegrationTest {
private lateinit var db: AppDatabase
private lateinit var server: MockWebServer
private lateinit var repository: StudentRepository
// Bug 1
@Before
fun setUp() {
server = MockWebServer()
// server.start() missing
val api = Retrofit.Builder()
.baseUrl("https://api.schoolapp.com/") // Bug 2
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(StudentApi::class.java)
db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build() // Bug 3 — missing allowMainThreadQueries() for test simplicity
repository = StudentRepositoryImpl(api, db.studentDao())
}
@After
fun tearDown() {
db.close()
// Bug 4 — server.shutdown() missing
}
@Test
fun `fetchStudents returns local data first then network data`() = runTest {
// Pre-populate local DB
db.studentDao().insert(Student("s1", "Alice", gradeLevel = 10))
server.enqueue(
MockResponse()
.setBody("""[{"id":"s1","name":"Alice Updated","gradeLevel":10}]""")
.setResponseCode(200)
)
repository.getStudents().test {
val localFirst = awaitItem()
assertThat(localFirst).hasSize(1)
assertThat(localFirst[0].name).isEqualTo("Alice")
// Bug 5
val networkUpdated = awaitItem()
assertThat(networkUpdated[0].name).isEqualTo("Alice Updated")
}
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Test your Room database (Android) (Android Developers)
- MockWebServer — OkHttp (Square)
- Room Migration Testing (Android Developers)
- Turbine Flow Testing (Cash App)
- Android Integration Testing Guide (Android Developers)