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:
- Directionality —
document.documentElement.dir == "rtl"(andlang == "ar") when the AR locale loads. - 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 thefinally— 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/).
Related
- 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
finallymust ignore. - Source:
tests/.../Infrastructure/IntegrationTestCleanup.cs,tests/.../Locale/ArRtlTests.cs;docs/ManpowerIQ_Handover_v10.md; PB-019 / PB-088 / PB-002.