Skip to content
Back to skills

Testing

ASecurity

Writes readable, refactoring-proof tests for a ddd-ts-kit project. Given / when / then for aggregates, real handlers through the test application with fakes, builders for test data, contract suites shared by fakes and adapters, end-to-end specs. Use when writing or reviewing a test, turning it.todo entries into tests, when a test needs data setup, when tempted by vi.mock, vi.fn or a spy, or when a test/* rule is reported.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsgobashreacttestingrefactoringapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 16, 2026

npx -y skills add gbourgeat/ddd-ts-kit --skill testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/gbourgeat-testing/badge)](https://www.skillsdirectory.com/skills/gbourgeat-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: testing
description: >-
  Writes readable, refactoring-proof tests for a ddd-ts-kit project. Given / when / then
  for aggregates, real handlers through the test application with fakes, builders for test
  data, contract suites shared by fakes and adapters, end-to-end specs. Use when writing or
  reviewing a test, turning it.todo entries into tests, when a test needs data setup, when
  tempted by vi.mock, vi.fn or a spy, or when a test/* rule is reported.
allowed-tools:
  - Read
  - Edit
  - Bash(npx ddd-ts-kit generate *)
  - Bash(npx ddd-ts-kit check *)
  - Bash(npx ddd-ts-kit list *)
  - Bash(npm test *)
  - Bash(npm run test:coverage)
  - Bash(npm run test:e2e *)
---

# Tests

A test reads as a business rule: what was true, what was asked, what happened. Examples use
Vitest, the default; with Jest the same rules hold. Every generated spec holds one passing
test and `it.todo` entries naming the behaviours to prove: replace each, never delete one.

## Levels

| Level | What runs | Doubles | Where |
|---|---|---|---|
| domain | aggregates, value objects, domain services | none | `test/unit/` |
| application | the real handler through the buses, with its permission | fakes of every port, fixed clock, sequential ids | `test/unit/` |
| adapter | one adapter against its real dependency, and the contract suite | none | `test/integration/` |
| end to end | the whole application through HTTP and the event bus | none; generated seeded data | `test/e2e/` at the project root |

## No mocks

No `vi.mock`, `vi.fn`, `vi.spyOn`, their `jest` twins or a mocking library in a context's
tests (`test/no-mocks`): they assert calls, not outcomes, and break on refactoring.

| Need | Use |
|---|---|
| a repository | its fake in `test/fakes/` (extends `InMemoryRepository`); assert `findById` or `all()` |
| another port | its fake, a working in-memory class recording what it received; an object literal fits too, a port being a shape |
| time, identifiers | `application.clock` (`advance`), sequential ids (`idPrefix`) |
| published events | `application.eventBus.published`; domain events of the command, heard once its handler has returned: `application.domainEvents.on('<name>', ...)` before executing; what `after-commit` handlers threw: `application.domainEvents.afterCommitFailures` |
| logs, metrics, unexpected failures | `application.logger.entries`, `metrics.samples`, `errors.reports` |
| a failing dependency | a method on the fake: `payments.failNextWith(new PaymentDeclined())` |
| a scheduled job | `application.jobs.runNow('<context>.<job>')` |
| the database | the real one in the adapter's integration spec, never a mocked driver |

A fake stays honest because it passes the same contract suite as the real adapter
(`@ddd-ts-kit/testing/contracts`); `generate port|repository` writes both.

## Aggregate specs: given / when / then

```ts
given(anOrder().withLine(aLine()).build())
  .when((order) => order.pay(clock.now()))
  .thenEvents([{ name: 'ordering.order.paid' }]);

given(anOrder().paid().build())
  .when((order) => order.pay(clock.now()))
  .thenError('ORDER_ALREADY_PAID', { orderId: 'order-1' });
```

An event-sourced aggregate starts from its history, without a database, and its snapshots are
proven once:

```ts
givenEvents((id, history) => Order.rehydrate(id, history), [placed, lineAdded])
  .when((order) => order.pay(clock.now()))
  .thenEvents([OrderPaid]);

expectSnapshotEquivalence({ events, rehydrate, policy: orderSnapshots, state: (order) => order.toState() });
```

One intention per test. Assert events, errors, and state through the public API only. A
refusal is asserted by its code and its facts, never by a message or the class of one error:
`thenError(code, facts)` on an aggregate, `expectRefusal(subject, code, facts)` on an action
(`() => Email.create(raw)`, the factory of a value object that throws), a `Result` or a caught error, `expectProblem(response, { status, code, facts })` end to end.

## Handler specs

The generated `arrange()` builds a test application from the platform test module, the
context (`contextModule('<context>', declarations)` or its module) and the fakes. Execute
through `application.commandBus` or `queryBus` with an actor built by `anActor()`. Cover: the
happy path, each failure by its error code, nothing saved or published on failure, and the
refusal of an actor without the permission (generated). Reactions: dispatch the event and
assert the command's effect; an integration event handler run twice changes state once.

Always assemble the context from its generated file: it is what registers what each
constructor is injected, so neither the test nor the handler lists a dependency. To test one
class alone, take it from that file,
`contextModule('<context>', { PlaceOrderHandler: declarations.PlaceOrderHandler })`, never
from its own import: a class handed over alone has no dependency list
(`WiringError MISSING_DEPENDENCIES`). Every port a class of the assembled context takes is
bound by the platform test module or by a `fake(Port, aFake)`, read models of projections
included.

## Builders

One per aggregate in `test/builders/` (`generate builder`, written with the aggregate):
valid defaults, methods named in the ubiquitous language (`paid()`, not
`withStatus('PAID')`), built through the aggregate's public API so an invalid state cannot be
built. A test states only what its rule depends on.

## End to end

`npx ddd-ts-kit generate e2e <ctx> <UseCase>` writes the three baseline cases of a route:
401 without a credential, 403 without the permission, the documented status with it. Add
the business scenario; assert what a client observes (responses, published events), never
the database.

## Shape

- `describe` the unit, `it` states the rule: `refuses to pay an order twice`.
- Arrange, act, assert separated by blank lines; no `if`, no loop; `it.each` for the
  boundaries of a value object.
- No shared mutable state; never read a private field; no snapshot of whole objects.

A coverage rate says that code ran under a test, never that the test asserts anything: write
the spec for the behaviour, and let the rate follow. The thresholds of `ddd-ts-kit.config.json`
are a floor per context and layer, not a goal; a test written to move a number proves nothing.

Finish with `npm test -- <context>`, `npm run test:coverage` before the slice is closed, and
`npx ddd-ts-kit check --rule test/no-mocks --rule test/builder-required --rule test/spec-required`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.