-
Notifications
You must be signed in to change notification settings - Fork 0
Test Integration
Ali Sadeghi edited this page Jan 27, 2026
·
7 revisions
Generates E2E integration tests (MockEngine → ViewModel).
Spawned by: test-orchestrator during /feature-test command
Real: ViewModel + Repository + DataSource implementations
Mocked: Only HTTP layer (MockEngine)
This catches wiring bugs that unit tests miss.
- 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
@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()
}
}
}Same as ViewModel tests - advanceUntilIdle() must be called immediately after method calls, NOT after ViewModel creation.
Back to Testing Agents | Agents