---
name: development-testing
description: >
  Generates real test suites per architectural layer with business rule coverage,
  consuming NuGet (xUnit, FluentAssertions, Moq, FluentValidation.TestHelper) and
  npm (Vitest, Testing Library, React Query) packages.
phase: development
cli: cli/
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Development Testing

Generates and manages test suites for SmartStack client extensions at every
architectural layer: seed, domain, business (with business rule tests),
API integration, and frontend components.

**All generated tests are functional** (no `Assert.True(true, "TODO")` stubs).
They compile, run, and assert real behaviour out of the box. They can fail
the first time if the underlying production code does not respect SmartStack
conventions — that is the intended signal.

## When to Use

- Phases 1 / 2a of `/ba-develop` for the per-layer test projects (`scaffold-tests`, once per layer) and Phase 4 for the acceptance tests (`scaffold-tests-from-ac`)
- When adding tests to an existing module
- When verifying business rule coverage

## Test Strategy by Layer

| Layer | Category Trait | Type Trait | Frameworks used | What it asserts |
|-------|---------------|------------|-----------------|-----------------|
| Seed | `[Trait("Category","Seed")]` | `[Trait("Type","Unit")]` | xUnit + EF Core InMemory | Idempotence (double-run no duplicates), full hierarchy, permission coverage |
| Domain | `[Trait("Category","Domain")]` | `[Trait("Type","Unit")]` | xUnit + FluentAssertions | Factory `Create()`, Update(), unique ids, field assertions |
| Business (handler) | `[Trait("Category","Business")]` | `[Trait("Type","Unit")]` | xUnit + Moq + FluentAssertions | Handler delegates to `I{E}Service` (Mock + Verify) |
| Business (service) | `[Trait("Category","Business")]` | `[Trait("Type","Unit")]` | xUnit + InMemory `ExtensionsDbContext` + fake tenant + FluentAssertions | Service persists / queries / deletes; named "Tenant" filter exercised + per-entity cross-tenant isolation fact (fails if the TENANT-FILTERS line is unmounted — DEV-API-032) |
| Business (validator) | `[Trait("Category","Business")]` | `[Trait("Type","Unit")]` | xUnit + FluentValidation.TestHelper | `ShouldHaveValidationErrorFor(...)` per required field + one `[Fact]` per rule |
| API | `[Trait("Category","Integration")]` | `[Trait("Type","Integration")]` | xUnit + WebApplicationFactory<Program> + **SQL Server LocalDB** + Respawn | Real routes (PostConfigure JWT): 401 unauthenticated + **denied facts** (`*_WithoutPermission_IsDenied`, exact 403/401 — a token with ZERO permission claims can never get 200: fails on an unguarded endpoint, the runtime twin of DEV-API-033) + the positive fact `GetAll_WithReadPermission_IsNotForbidden` when the entity spec declares its `section` (exact 4-seg permission, never guessed — a 403 with the right claim = broken wiring) + no-5xx probes |
| Frontend | — | — | Vitest + Testing Library + React Query | Render, page loads, QueryClient wiring |
| **UI (after Phase 3e — `ui-test`)** | — | — | dev-browser CLI + Studio runner | Real Chromium navigates each scaffolded page with role-based seeded users, asserts no 4xx/5xx, submits forms, auto-corrects via `claude -p fix-bug` (max 50 iter/test). See `ui-test/SKILL.md`. |

## Test conventions (source of truth)

These generators encode the idioms distilled from the SmartStack.app test corpus.
The deployed client skill **`/test-conventions`** is the per-type reference —
read its `references/<type>.md` before hand-writing or extending any test.
Universal rules applied across every backend layer: `Method_Scenario_Expected`
naming · one test = one behaviour · `#region` per method · factory
`Entity.Create(...)` (never `new`) · `Guid.NewGuid()` (never hardcoded) ·
FluentAssertions 8.x (DateTime → `BeOnOrAfter`/`BeBefore`/`BeCloseTo`, never
`BeGreaterThan`; collections `HaveCount`; exceptions `Throw<T>().WithMessage("*kw*")`)
· xUnit v3 `TestContext.Current.CancellationToken` · `NullLogger<T>.Instance` ·
`Options.Create(...)` · **zero stubs** (`Assert.True(true)` / `expect(true).toBe(true)`).
`audit-dev-tests` flags drift on the last few as DEV-TEST-004 (err — stub
assertions block) and DEV-TEST-005..007 (advisory).

## Key behaviour contracts

### Seed tests
In-memory `CoreDbContext` (`UseInMemoryDatabase(Guid.NewGuid().ToString())`),
`TestContext.Current.CancellationToken`. Call `SeedNavigationAsync` twice to assert
idempotence; check FK alignment (every module has a matching application).

### Domain tests
`${Entity}.Create(...)` with valid args, assert properties + `BeOnOrAfter(before)`
for timestamps; `#region` per method. Call with `null!`/default for required params
to assert `ArgumentException`.

### Business tests — two levels
The generated handler is a thin delegator to `I{E}Service`; the service holds the
logic + EF access. So the tests split:
- **Handler tests** (`{E}HandlersTests`) — bare `Mock<I{E}Service>` + `Verify(...)`
  the delegation. No DbContext.
- **Service tests** (`{E}ServiceTests`) — a REAL in-memory `ExtensionsDbContext`
  built with a `FakeCurrentTenantService` (so the tenant query filter is exercised;
  EF InMemory honours query filters). Asserts Create persists / GetById returns /
  Delete removes. The service injects **`IExtensionsDbContext`** (extension entities
  live in the `extensions` schema; Core lookups go through `ICoreDataService`).
  InMemory does NOT enforce relational constraints, SQL translation, or
  SqlObjects/TVF — the integration layer (below) covers those.
- **Validator tests** — `TestValidate(...).ShouldHaveValidationErrorFor(...)` per
  required field + one `[Fact]` per business rule.

### API / integration tests — REAL SQL Server LocalDB (mandatory)
Integration tests run against a **real SQL Server LocalDB** (never InMemory) via a
`DatabaseFixture` (Respawn reset) + a `{Ext}WebAppFactory : WebApplicationFactory<Program>`:
- Override JWT via `services.PostConfigure<JwtBearerOptions>(...)` — NOT via config
  (read too early by `AddInfrastructure`).
- Mint a token with the shared HMAC key (`JwtTokenHelper`), call REAL routes, assert
  status codes; dispose factory + client (`IAsyncDisposable`).
- Execution pitfalls: a running dev API locks the build output → **MSB3021** (stop
  it, or `dotnet test -p:BuildProjectReferences=false`); use `npm install` (not
  `npm ci`) while a Vite dev server runs (`@tailwindcss/oxide` lock).

The harness templates ship under `templates/project/test-backend/`
(`DatabaseFixture`, `{Ext}WebAppFactory`, `JwtTokenHelper`, `FakeCurrentTenantService`)
and are scaffolded into the client test project.

### Frontend tests
`vi.mock(...)` **HOISTED above the page import**; **mock `react-i18next`**
(`t: (k) => k`); `MemoryRouter` (never `BrowserRouter`); Testing Library queries;
`crypto.randomUUID()`. The shared harness (`test-frontend/test-utils.tsx`) keeps
**MSW** + React Query for integration-style page tests.

## CLI: scaffold-tests

```bash
npx --prefer-offline tsx skills/development/testing/cli/scaffold-tests/index.ts \
  --spec '{"layer":"business","module":"hrm","appCode":"MyApp","applicationCode":"hr","entities":[{"name":"Employee","pluralName":"Employees","fields":[{"name":"firstName","type":"string"}]}],"businessRules":[],"projectPath":"/path"}'
```

Layer values: `seed | domain | business | api | frontend`. Spec: `layer`, `module`,
`appCode` (.NET project name), `applicationCode` (BA app code, kebab-case),
`entities[]` (`{ name, pluralName, fields[] }` — ALL THREE required; `fields[]` =
`{ name, type, required? (default true), enumValues? }`; optional `section?`, `hasCreate?`
(default true), `dataScope?`), `tenantMode?` (default `strict`),
`businessRules[]?` (default `[]` — `{ id, entityName, description, validExamples?, invalidExamples? }`),
`projectPath` (must exist), `namespace?`. Flags: `--spec` / `--spec-file`, `--outdir`, `--dry_run` —
there is NO `--layer` flag (the layer is a spec field). The CLI validates and generates
from the Zod-PARSED spec: every documented default (`tenantMode`, `businessRules`,
`fields[].required`, `hasCreate`) is applied before the semantic checks and generation
run (2026-09-24 — a business spec without `businessRules` used to crash on `.length`).

## CLI: scaffold-tests-from-ac

Generates ONE xUnit `[Fact]` per Acceptance Criterion declared under each Use
Case in `.smartstack/ba/<APP>/<MODULE>/<section>/use-case.md`. The body is a
`// TODO[AC-NN]` skeleton that **Phase 4 of /ba-develop** fills in with the
real assertion. The audit `DEV-TEST-001` (Wave 3) blocks merge if any TODO
remains. AC live in `use-case.md` under a `**Acceptance Criteria**` field —
each bullet is `- [ ] AC-NN — <imperative assertion>`.

```bash
npx --prefer-offline tsx skills/development/testing/cli/scaffold-tests-from-ac/index.ts \
  --spec '{"moduleDir":".smartstack/ba/CRM/PIPELINE","projectPath":"./generated/crm","appCode":"crm","module":"pipeline"}' --json
```

Spec fields:
- `moduleDir` (required) — absolute or repo-relative path to the BA module folder.
- `projectPath` (required) — root of the .NET solution.
- `appCode` (required, lowercase kebab) — used in the namespace prefix.
- `module` (required, lowercase kebab) — used in the namespace prefix.
- `namespace` (optional) — full namespace override; defaults to `{Pascal(appCode)}.Tests.{Pascal(module)}.Acceptance`.
- `dryRun` (optional) — parse + plan, do not write.

Stdout is the JSON result — ALWAYS (`success`, `errors[]`, `ucsScanned`, `acsParsed`,
`factsEmitted`, `filesCreated[]`, `warnings[]`); `--json` is accepted and has no effect
(2026-09 — the gate reads `success` from stdout; the human summary used to be the default).

Output: `Tests/{App}/{Module}/{Section}/{Section}AcceptanceTests.cs` (one per section
that has ≥ 1 AC). Every Fact carries `[Trait("AC", "<UC-code>#AC-NN")]` so the
runner can filter and the audit can match BA ACs against generated tests 1:1.

The global AC reference format is `<UC-code>#AC-NN` (e.g.
`UC-CRM-PIPELINE-OPPORTUNITES-001#AC-02`) — both the BA file and the generated
test agree on this id. See `business-analyse/_workflow/doc-templates.md` for the
authoritative AC grammar and `business-analyse/audit-use-cases/SKILL.md`
UC-013..018 for the audit rules.

## CLI: test-report

```bash
npx --prefer-offline tsx skills/development/testing/cli/test-report/index.ts \
  --spec '{"projectPath":"/path","module":"hrm"}'
```

Produces structured JSON report at `.smartstack/reports/TEST-{module}-{timestamp}.json`.

Accounting (never a fabricated pass): a RED vitest suite exits 1 with its JSON
still on stdout — the counts are kept and the report names each failing test by
its FULL name (`describe` titles + title, read from
`testResults[].assertionResults[]` — the reporter emits one `testResults[]` entry
per FILE, not per test) with its file relative to the project and the assertion
message without its stack frames, never an empty reason (fallback: the file-level
message, then `Test failed`); a file that throws before any test runs (import
error — `numFailedTests` is 0 yet `success` is false) is one failure named by the
file, carrying the file-level message; `skipped` counts vitest's pending + todo
tests (QC 2026-09-24, D5: the parser read each entry as a test and a red run named
the file path with an empty reason). An unparsable vitest output, or JSON that is
not a vitest report, is `failed: 1` with a non-empty reason (the runner's stderr,
else the by-hand command); a `dotnet test` failure carries the message line, not
the `Error Message:` header; `coverage` is `null` (no tool integrated). A run that executed 0 tests on every layer (the module filter matched no test file)
answers `success:false` ("0 tests ran") — a gate that ran nothing cannot pass (2026-09-24).

## Test File Naming Convention

```
Tests/Common/FakeCurrentTenantService.cs                ← shared tenant test-double (emitted once)
Tests/{App}/{Module}/Seed/{Module}SeedDataProviderTests.cs
Tests/{App}/{Module}/Domain/{Entity}Tests.cs
Tests/{App}/{Module}/Application/{Entity}HandlersTests.cs     ← handler delegation (Mock<I{E}Service>)
Tests/{App}/{Module}/Application/{Entity}ServiceTests.cs      ← service via in-memory ExtensionsDbContext
Tests/{App}/{Module}/Application/Create{Entity}CommandValidatorTests.cs
Tests/{App}/{Module}/Api/{Plural}ControllerTests.cs           ← integration: LocalDB + Respawn + WebAppFactory
Tests/{App}/{Module}/{Section}/{Section}AcceptanceTests.cs   ← scaffold-tests-from-ac
tests/{app}/{module}/{entityLower}/{Entity}.test.tsx                ← one per ENTITY (what DEV-UI-007 accepts)
```

## NuGet / npm packages required in the client project

Backend test project:
- `xunit.v3`, `xunit.runner.visualstudio` (xUnit v3 — `TestContext.Current.CancellationToken`)
- `FluentAssertions` (>= 8.x — see `BeOnOrAfter`, `HaveCount` gotchas)
- `Moq`
- `FluentValidation.TestHelper`
- `Microsoft.EntityFrameworkCore.InMemory` (unit-layer service tests)
- `Microsoft.AspNetCore.Mvc.Testing` (API/integration tests)
- `Respawn` + `Microsoft.Data.SqlClient` (integration tests — real SQL Server LocalDB reset)

Frontend test setup:
- `vitest`, `@testing-library/react`, `@testing-library/user-event`, `@testing-library/jest-dom`
- `@tanstack/react-query`
- `react-router-dom`
- `@atlashub/smartstack` (for `SmartStackProvider` in test providers)
