🧪

Integration Testing (WebApplicationFactory, TestContainers) (EN)

Testing Intermediate 3 min read 500 words

TL/Principal integration testing strategy: what to cover, how to avoid flakiness, and how to keep it fast

Integration Testing (WebApplicationFactory, TestContainers) (EN)

Integration tests validate that components work together (HTTP pipeline, DI, serialization, database, messaging), without needing full end-to-end UI.

At lead level, the key is to design a test strategy that is:

  • Representative (real dependencies where it matters)
  • Reliable (no flakes)
  • Fast enough for CI (parallelizable, scoped)

1) When you should prefer integration tests

Integration tests are best for:

  • ASP.NET Core startup + middleware + routing
  • Model binding, validation, filters
  • Serialization behavior (System.Text.Json options)
  • EF Core mapping, migrations, query translation
  • Authentication/authorization policies (without real identity providers)

Unit tests are still best for:

  • pure domain rules
  • complex algorithms
  • small deterministic transformations

2) The “right realism” principle

Make integration tests realistic where bugs happen:

  • Use a real DB engine for query translation/perf behavior (TestContainers)
  • Use in-process server (WebApplicationFactory) for HTTP pipeline

Avoid realism that adds little value:

  • real external network
  • real third-party APIs (use contract tests or stubs)

3) WebApplicationFactory mental model

WebApplicationFactory<TEntryPoint> (ASP.NET Core) hosts your app in-memory so your tests can call endpoints with HttpClient.

Typical hooks:

  • Override configuration (connection strings, flags)
  • Replace services for testing (e.g., fake email sender)
  • Ensure DB is created/migrated

4) TestContainers strategy

Use TestContainers when you need:

  • real SQL Server/Postgres behavior
  • realistic isolation levels
  • real indexes and query plans

Lead-level pitfalls

  • Slow startup: mitigate with reusable containers per test collection.
  • Port collisions: let TestContainers pick ports.
  • Parallelization issues: isolate DB per test or per collection.

5) Test data strategy

At scale, tests fail because data management is chaotic.

Recommended pattern:

  • Use Arrange helpers + builders
  • Make tests self-contained (create data they need)
  • Reset DB between tests using:
    • transactions + rollback (fast, but tricky with async/background work)
    • truncate tables (deterministic, sometimes slower)
    • recreate schema per test collection

6) Flake-proofing integration tests

Common sources of flakiness:

  • time-based logic → inject clock
  • async eventual consistency → poll with timeouts and clear contracts
  • random ports / reused state
  • parallel tests sharing data

Rule of thumb:

  • If you must poll, poll for a bounded time and include meaningful diagnostics.

7) What “good coverage” looks like

A TL-friendly integration suite usually covers:

  • health endpoint
  • one “happy path” per critical use case
  • one validation failure path
  • one authorization failure path
  • one persistence scenario that validates mapping + constraints

Avoid building a second, slower unit test suite.


8) Interview angle

Be ready to discuss:

  • why you choose TestContainers over EF InMemory
  • how you keep integration tests fast
  • how you manage DB state
  • how you structure test suites (collections, parallelization)

9) Review checklist

  • [ ] Integration tests run in CI within acceptable time (target: minutes).
  • [ ] TestContainers used only where needed (DB/query translation).
  • [ ] Data isolation strategy is documented and consistent.
  • [ ] No hidden dependencies on developer machine state.
  • [ ] Parallelization is controlled (collections) and deterministic.

📚 Related Articles