Skip to content

Backend: measure test coverage, report before gating #776

Closed

Description

@mforce

What

The backend has no coverage measurement. Add it, so the 2584 backend tests can be reasoned about with data instead of impressions — starting with the 1793 integration tests that dominate CI wall clock.

This is the blocking first step of #775 (CI duration). It is also worth doing on its own: the web side has been measured and gated since #121 and the backend never has.

The gap, read from the repo

No coverlet, no --collect:"XPlat Code Coverage", no reportgenerator anywhere in Directory.Packages.props, the four test .csproj files, or .github/workflows/ci.yml. dotnet test Cluckwork.sln reports pass/fail counts only.

By contrast the SPA is measured and gated: web/vite.config.ts:148 configures @vitest/coverage-v8 with a regression floor and per-directory locks, and ci.yml:179 runs npm run test:coverage. Measured 2026-09-12: 91.2% statements, 87.64% branch, 86.56% functions, 94.16% lines.

Backend test counts: Domain 491, Application 290, AppHost 10, Integration 1793.

Scope

  1. Collect coverage for all four backend test projects. Central Package Management applies (chore: adopt Central Package Management so a version bump is one file, not eight #684), so the package version goes in Directory.Packages.props and the .csproj files carry a bare PackageReference; lock files regenerate in the same commit or CI fails NU1004.
  2. Produce one readable report (per-project and combined) and publish it as a CI artifact.
  3. Report only at first. Do not add a gate in this issue. A threshold picked before anyone has seen the numbers is a guess, and per AGENTS.md a wrong guard is worse than none because it reads as safety. Web's floor was set from measured values after the fact; do the same here.

Decide before writing code

1 — does the integration project's coverage even mean what it looks like? Those tests exercise the API over a real Postgres via Testcontainers, so they touch a great deal of code incidentally. High line coverage there will overstate how much is actually asserted. Say so in the report rather than letting a large number imply safety.

2 — one combined number or per-project? A combined figure hides that Domain and Application are unit-tested while Integration is end-to-end. Per-project is probably more useful; decide explicitly.

3 — does it run on every PR or on a schedule? Collection adds time to the job that #775 exists to shorten. A nightly or workflow_dispatch run may be the better trade until the numbers are known.

What this does NOT settle

Coverage cannot decide which tests to delete. It measures what is executed, not what is load-bearing. Two tests can cover identical lines while only one catches a defect — demonstrated on this repo in #771, where adversarial review found three guards that were fully covered and still survived the exact mutations they were named for (an ef_filter assertion satisfied by unrelated tables, a > 0 check satisfied by any expression over zero, and a status case that only ever constructed a Draft).

So a coverage report is the input to "what is untested", not to "what is redundant". If the redundancy question is the real one, the follow-up tool is mutation testing (e.g. Stryker.NET) over a scoped subset, and that deserves its own issue once these numbers exist.

Repo rules this touches

Split out of #775 so the measurement can land independently of the CI-duration work it informs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codeenhancementNew feature or requestepic-15github_actionsPull requests that update GitHub Actions codesize:SHours to a day; few files, no migrationsliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions