Skip to content

Convention — test patterns

What it is

Three testing conventions the codebase relies on, each born from a real defect class: FK-block try/finally ownership (so tests don't drain shared seed data), AR-locale content-swap sentinels (so i18n tests don't pin fragile translations), and the InMemory-vs-Postgres provider gap (so aggregate queries are tested against real Npgsql, not just EF InMemory).

When to use them

On integration and E2E tests. The InMemory-vs-Postgres rule applies to any service doing aggregation/grouping; the ownership rule to any test mutating shared-DB rows; the locale rule to any test asserting Arabic rendering.

1. FK-block try/finally ownership

Rule: a test owns the rows it asserts on and restores them in a finally block. The integration tests run against a shared dev Postgres that is not reset between sweeps, so a test that consumes a seeded row without restoring it drains the fixture over repeated runs.

The defect that motivated it (PB-088): a test consumed one NULL-reason demand_template_lines seed row each run without restoring it; six runs drained the seed 6 → 5 → 1 and the test then failed intermittently (tests/.../Infrastructure/IntegrationTestCleanup.cs).

Shape: insert/mutate inside try; in finally, read back with IgnoreQueryFilters() (so soft-deleted rows are visible) and restore — un-soft-delete the original, hard-delete any synthetic sibling. Use a suffixed test code (<ENT>_TEST_…) so the cleanup helper can sweep residue.

The four ownership rules: tests own the rows they assert on; migration Down() is the structural inverse; sweeps don't start mid-migration; verification checklists derive from seeded data (IntegrationTestCleanup.cs).

2. AR-locale content-swap sentinel

Rule: don't pin exact Arabic strings — pin the two invariants. Translations get copy-edited; a test asserting Assert.Equal("تسجيل الدخول", title) is brittle. Instead assert:

  1. Directionalitydocument.documentElement.dir == "rtl" (and lang == "ar") when the AR locale loads.
  2. Content swap — the same DOM element renders different text in EN vs AR (Assert.NotEqual(enTitle, arTitle)), proving i18n actually resolved rather than falling back to keys.

This is "decision #98", locked in the PB-019 work and applied in tests/.../Locale/ArRtlTests.cs and reused in LeaveLocaleTests.cs. Note the locale vs language distinction: Playwright context takes ar-SA (BCP 47), localStorage takes ar (2-letter) — the helper maps between them.

3. InMemory-vs-Postgres provider gap

Rule: cover provider-sensitive queries with a real-Postgres integration test. EF Core's InMemory provider doesn't translate every query the same way Npgsql does, so a green InMemory unit test can mask a runtime translation failure. The enshrined example: GroupBy(_ => 1) group-all aggregates behave differently on an empty set (InMemory vs Npgsql → null vs 0), which broke equalization bounds (Handover v10; and confirmed in this project's own memory note).

Practice:

  • Use InMemory for non-aggregating logic (validators, single-row queries, orchestration) — fast and parallel.
  • For any aggregation/grouping (equalization cohorts, counts feeding UI-polled values), add a [Collection("PostgresIntegration")] test that seeds realistic data and asserts the shape against real Postgres.
  • Treat GroupBy(_ => 1) / GroupBy(_ => true) as a review flag — add an explicit empty-set check and a Postgres test.

Gotchas / constraints

  • IgnoreQueryFilters() in the finally — without it the global soft-delete/tenant filter hides the row you're trying to restore.
  • "Works in InMemory" is not the bar for aggregates — only a Postgres integration test is.
  • Don't assert exact translations — assert difference + direction. Don't pin pixel positions or visual diffs either.
  • PostgresIntegration tests are serialized by their xUnit collection (PB-002), so adding one doesn't meaningfully slow the suite — skipping it just defers a runtime bug to the next developer.

Build status

Available — all three patterns are live in the test suites (tests/ManpowerIQ.Domain.Tests/, tests/ManpowerIQ.E2E.Tests/).

  • Audit-first discipline — audits check these conventions are followed.
  • Background jobs — the import job is provider-sensitive (InMemory branch).
  • Multi-tenancy — the query filter the finally must ignore.
  • Source: tests/.../Infrastructure/IntegrationTestCleanup.cs, tests/.../Locale/ArRtlTests.cs; docs/ManpowerIQ_Handover_v10.md; PB-019 / PB-088 / PB-002.