Skip to content

Test Integration

Ali Sadeghi edited this page Jan 27, 2026 · 7 revisions

Test Integration Agent

Generates E2E integration tests (MockEngine → ViewModel).

Spawned by: test-orchestrator during /feature-test command

Key Principle

Real:   ViewModel + Repository + DataSource implementations
Mocked: Only HTTP layer (MockEngine)

This catches wiring bugs that unit tests miss.

What It Tests

  • Full flow happy path (Load → Success)
  • Error recovery (Retry after failure)
  • HTTP error handling (401, 404, 500, 503)
  • Request verification (URL, method, parameters)
  • Empty and edge cases

Example Output

@OptIn(ExperimentalCoroutinesApi::class)
class LoginIntegrationTest {
    private val testDispatcher = StandardTestDispatcher()
    private lateinit var mockEngine: MockEngine
    private lateinit var viewModel: LoginViewModel

    @BeforeTest
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

    @AfterTest
    fun teardown() {
        Dispatchers.resetMain()
    }

    private fun setupWithMockEngine(
        handler: suspend MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
    ) {
        mockEngine = MockEngine { request -> handler(request) }
        val httpClient = HttpClient(mockEngine) {
            install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
            install(Resources)
        }

        // Wire REAL implementations
        val apiClient = ApiClient(httpClient)
        val dataSource = LoginRemoteDataSourceImpl(apiClient)
        val repository = LoginRepositoryImpl(dataSource)
        viewModel = LoginViewModel(repository)
    }

    @Test
    fun `full flow - login succeeds`() = runTest(testDispatcher) {
        setupWithMockEngine { request ->
            respond(
                content = LoginFixtures.validLoginResponseJson,
                status = HttpStatusCode.OK,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }

        viewModel.uiModelState.test {
            var current = awaitItem()

            if (current.state is UiState.Loading) {
                advanceUntilIdle()
                current = awaitItem()
            }

            assertTrue(current.state is UiState.Success)
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun `full flow - retry after failure succeeds`() = runTest(testDispatcher) {
        var requestCount = 0
        setupWithMockEngine { request ->
            requestCount++
            if (requestCount == 1) {
                respond(
                    content = LoginFixtures.error503Json,
                    status = HttpStatusCode.ServiceUnavailable,
                    headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                )
            } else {
                respond(
                    content = LoginFixtures.validLoginResponseJson,
                    status = HttpStatusCode.OK,
                    headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                )
            }
        }

        viewModel.uiModelState.test {
            // Wait for initial failure
            var current = awaitItem()
            while (current.state !is UiState.Failed) {
                advanceUntilIdle()
                current = awaitItem()
            }

            // Retry
            viewModel.retry()
            advanceUntilIdle()

            // Wait for success
            while (current.state !is UiState.Success) {
                current = awaitItem()
            }

            assertTrue(current.state is UiState.Success)
            assertEquals(2, requestCount)

            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun `full flow - handles unauthorized error`() = runTest(testDispatcher) {
        setupWithMockEngine { request ->
            respond(
                content = LoginFixtures.error401Json,
                status = HttpStatusCode.Unauthorized,
                headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString())
            )
        }

        viewModel.uiModelState.test {
            skipItems(1)
            advanceUntilIdle()

            val failed = awaitItem()
            assertTrue(failed.state is UiState.Failed)

            // HTTP 401 always maps to ErrorConst.Unauthorized
            val error = (failed.state as UiState.Failed).error as ErrorModel.MessageCode
            assertEquals("You must login", error.message)
            assertEquals(1001, error.code)

            cancelAndIgnoreRemainingEvents()
        }
    }
}

Critical: Test Dispatcher Usage

Same as ViewModel tests - advanceUntilIdle() must be called immediately after method calls, NOT after ViewModel creation.


Back to Testing Agents | Agents

Clone this wiki locally