androidengineers.Book a session

Testing & Automation

Integration Testing

article40 minHard

Integration tests verify that multiple components work together correctly. Where unit tests isolate one class with mocks, integration tests use real implementations — a real database, a real HTTP client against a fake server, real coroutines with real dispatchers.

They are slower than unit tests but catch a class of bugs that mocks cannot: incorrect SQL queries, wrong HTTP request shapes, mapping errors between layers, and transaction behavior.

The Test Pyramid

       E2E / UI tests      ← few, slow, high confidence
     Integration tests     ← some, medium speed
   Unit tests              ← many, fast, low coupling

Integration tests live in the middle. You should have more unit tests than integration tests, but integration tests give you confidence that real systems communicate correctly.

Room Integration Tests

Test Room with an in-memory database. It behaves identically to a disk database but is destroyed after each test:

@RunWith(AndroidJUnit4::class)
class TaskDaoTest {

    private lateinit var db: AppDatabase
    private lateinit var dao: TaskDao

    @Before
    fun setup() {
        db = Room.inMemoryDatabaseBuilder(
            ApplicationProvider.getApplicationContext(),
            AppDatabase::class.java
        ).allowMainThreadQueries().build()

        dao = db.taskDao()
    }

    @After
    fun teardown() {
        db.close()
    }

    @Test
    fun insertAndObserveTasks() = runTest {
        val task = TaskEntity(id = "1", title = "Write tests", completed = false)
        dao.upsert(task)

        val tasks = dao.observeTasks().first()
        assertEquals(1, tasks.size)
        assertEquals("Write tests", tasks[0].title)
    }

    @Test
    fun deleteRemovesTask() = runTest {
        val task = TaskEntity(id = "1", title = "Delete me", completed = false)
        dao.upsert(task)
        dao.delete(task)

        val tasks = dao.observeTasks().first()
        assertTrue(tasks.isEmpty())
    }

    @Test
    fun upsertReplacesExistingTask() = runTest {
        val original = TaskEntity(id = "1", title = "Original", completed = false)
        val updated = original.copy(title = "Updated", completed = true)

        dao.upsert(original)
        dao.upsert(updated)

        val tasks = dao.observeTasks().first()
        assertEquals(1, tasks.size)
        assertEquals("Updated", tasks[0].title)
        assertTrue(tasks[0].completed)
    }
}

allowMainThreadQueries() is acceptable in tests. Never use it in production code.

MockWebServer for Retrofit Tests

OkHttp's MockWebServer lets you control exactly what the server returns, without a real network or server.

class UserRepositoryTest {

    private lateinit var mockWebServer: MockWebServer
    private lateinit var api: UserApi
    private lateinit var repository: UserRepository

    @Before
    fun setup() {
        mockWebServer = MockWebServer()
        mockWebServer.start()

        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

        api = retrofit.create(UserApi::class.java)
        repository = UserRepositoryImpl(api)
    }

    @After
    fun teardown() {
        mockWebServer.shutdown()
    }

    @Test
    fun `getUser returns mapped User on 200`() = runTest {
        mockWebServer.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody("""{"id":"1","name":"Akshay","email":"a@b.com"}""")
        )

        val result = repository.getUser("1")

        assertTrue(result.isSuccess)
        assertEquals("Akshay", result.getOrNull()?.name)
    }

    @Test
    fun `getUser returns failure on 500`() = runTest {
        mockWebServer.enqueue(MockResponse().setResponseCode(500))

        val result = repository.getUser("1")

        assertTrue(result.isFailure)
    }

    @Test
    fun `getUser sends correct request path`() = runTest {
        mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))

        repository.getUser("user-42")

        val request = mockWebServer.takeRequest()
        assertEquals("/users/user-42", request.path)
        assertEquals("GET", request.method)
    }
}

enqueue sets the next response. takeRequest lets you inspect what the client actually sent — useful for verifying headers, paths, and request bodies.

Testing Repository with Both Layers

Test the repository's caching and sync logic with both in-memory Room and MockWebServer together:

@Test
fun `sync fetches from network and writes to database`() = runTest {
    mockWebServer.enqueue(
        MockResponse().setBody("""[{"id":"1","title":"Task A","completed":false}]""")
    )

    repository.sync()

    val storedTasks = dao.observeTasks().first()
    assertEquals(1, storedTasks.size)
    assertEquals("Task A", storedTasks[0].title)
}

This test crosses three real layers (Retrofit → repository → Room) and catches mapping errors that unit tests with mocks would miss.

Hilt in Integration Tests

Replace production modules with test fakes using @TestInstallIn and run tests with HiltAndroidRule:

@HiltAndroidTest
class ProfileRepositoryTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var repository: ProfileRepository

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @Test
    fun `repository returns injected fake data`() = runTest {
        val result = repository.getProfile("user-1")
        assertTrue(result.isSuccess)
    }
}

@TestInstallIn replaces the production module with your test module for the duration of the test. The rest of the DI graph remains unchanged.

What Belongs in Integration Tests

TestType
SQL query correctnessIntegration (Room)
HTTP request shape + response mappingIntegration (MockWebServer)
Repository caching logicIntegration (Room + MockWebServer)
ViewModel business logicUnit (MockK)
Composable renderingUI (ComposeTestRule)
Full user flowE2E (Espresso / Compose)

Practice

Write an integration test for a NoteRepository that:

  1. Calls sync() which fetches notes from a MockWebServer endpoint
  2. Verifies the notes are stored in an in-memory Room database
  3. Asserts that observeNotes() returns the fetched notes
  4. Enqueues a 404 response and asserts sync() returns a Result.failure

Summary

Integration tests use real implementations — in-memory Room for database tests, MockWebServer for HTTP tests — and catch bugs that mocks cannot. Test DAO queries directly, verify request paths and response mapping with MockWebServer, and combine both layers to test repository caching and sync logic. Keep them focused and fast; avoid replacing unit tests with integration tests.

YOUR LEARNING JOURNEY

0 of 17 available lessons completed

Progress saved in this browser. No account needed.
Integration Testing | Senior Android Developer | Android Engineers