| name | integration-testing |
| description | Expert guidance on integration tests that exercise the seams between layers (repo → network → DB → mapper) without the UI. Use when unit tests pass but a feature still breaks, or when a bug is only visible when multiple collaborators interact. |
Integration Testing (Non-UI)
Instructions
Integration tests sit between unit tests and UI/E2E tests. They wire up real collaborators for one slice — typically repository + network client + DB + serialization — while keeping the UI, device platform, and remote services out of the picture. They catch mapper, transaction, and protocol bugs in milliseconds, not minutes.
1. What Counts as "Integration"
A good integration test:
- Spans more than one production class.
- Uses real implementations where they are cheap (JSON parser, SQL driver, HTTP stack against a local mock server).
- Uses fakes only for things that are genuinely external (auth server, push, analytics).
- Runs on the host runtime (JVM / host Swift / Node / Dart VM), not a device.
2. Local HTTP Mock Servers
Prefer a real HTTP server over in-process interception — it catches serialization and header bugs that interceptors skip.
- Kotlin / Java —
okhttp3.mockwebserver.MockWebServer.
- Swift —
URLProtocol subclass, or Swifter / Embassy for a real socket.
- Dart —
package:shelf in a local server or http_mock_adapter for Dio.
- TypeScript —
msw (preferred), or nock for low-level Node interception.
Kotlin example with MockWebServer + Retrofit + Room:
class OrdersSyncIntegrationTest {
private val server = MockWebServer()
private lateinit var db: AppDatabase
private lateinit var sut: OrdersRepository
@Before fun setUp() {
server.start()
db = Room.inMemoryDatabaseBuilder(ctx, AppDatabase::class.java).build()
val api = Retrofit.Builder().baseUrl(server.url("/")).addConverterFactory(MoshiConverterFactory.create()).build().create(OrdersApi::class.java)
sut = OrdersRepository(api = api, dao = db.orderDao(), clock = FixedClock(T0))
}
@After fun tearDown() { server.shutdown(); db.close() }
@Test fun sync_persists_and_deduplicates() = runTest {
server.enqueue(MockResponse().setBody(readFixture("orders/list_v1.json")))
sut.sync()
sut.sync()
val stored = db.orderDao().all()
assertEquals(3, stored.size)
}
}
3. MSW for React Native
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('https://api.example.com/orders', () =>
HttpResponse.json([{ id: 1, totalCents: 1999 }]))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('sync persists orders', async () => {
const repo = new OrdersRepository(api, db);
await repo.sync();
expect(await db.orders.count()).toBe(1);
});
4. Time, Retries, and Flakiness
- Inject a clock / ticker so retry tests do not actually sleep.
- Drive any exponential-backoff retry with a virtual scheduler (
TestDispatcher, RxJS TestScheduler, fakeAsync).
- Never put real
Thread.sleep / Task.sleep / Future.delayed in integration tests.
5. Seams Worth Integration-Testing
- Repository + remote + local cache — "fetch, cache, offline read, refresh".
- DTO → domain → DTO round-trips — serialization edge cases (nullable fields, enum unknowns, ISO-8601 timezones).
- Migration + data-access — run a real migration, then a real query.
- Token refresh flows — auth interceptor on 401 → refresh → retry.
6. Avoid Over-Integration
If the same thing can be verified by a unit test, write the unit test. Integration tests cost more to maintain:
- Slower by orders of magnitude.
- Harder to pinpoint failures (multiple suspects).
- More setup scaffolding to keep in sync.
Rule of thumb: one happy-path integration test per seam; everything else goes in unit tests unless it genuinely requires the seam.
7. Test Data Shared Across Layers
Commit JSON fixtures under testdata/ and reuse them in unit mappers, integration flows, and contract tests. Drift is the enemy — one fixture per shape, referenced everywhere.
8. Error-Path Integration
Exercise at least these error paths per seam:
- Network 5xx with and without
Retry-After.
- Malformed JSON.
- Empty body / 204.
- Timeout.
- Disk-full / SQL constraint violation (in DB integration).
9. Checklist