building-stories-with-tdd
Installation
SKILL.md
Story-Based Test-Driven Development Expert
You are an expert in Test-Driven Development (TDD) for NestJS applications using @lenne.tech/nest-server. You help developers implement new features by first creating comprehensive story tests, then iteratively developing the code until all tests pass.
Gotchas
- Detect the test framework BEFORE writing the first test — Projects use either Vitest or Jest. Vitest uses globals (
describe,it,expect) without imports; Jest requiresimport { describe, it, expect } from '@jest/globals'. Mixing styles produces misleading error messages ("global not defined") that look like runtime failures. Checkvitest.config.tsvsjest.config.tsfirst. - Test data emails use
@test.com— The cleanup regex inTestHelperuses@test.comas its deletion filter. Using@example.comor@user.defor test data leaves records in the test DB after the run ends, polluting subsequent test runs. This applies to both backend story tests and frontend Playwright fixtures. - Never use
declareon test-created Models — Same gotcha asgenerating-nest-servers:declareremoves the field at compile-time, so Typegoose decorators are lost. Test data that persists "successfully" but is missing fields in DB queries almost always traces back to adeclare. - Story test files must be in
tests/stories/— The runner auto-discovers from this path. Placing them intests/orsrc/__tests__/means they silently don't run. Backend:projects/api/tests/stories/<feature>.e2e-spec.ts. Frontend E2E:projects/app/tests/<feature>.spec.ts. - Iterate through the full test loop, not individual fixes — When a test fails, the instinct is to fix just that assertion. In TDD with generated code, re-run the FULL test suite after any implementation change. A passing test can break a previously-passing one through Model/Service changes that don't generate compile errors.
- A Playwright spec that asserts "no errors on the page" must watch
pageerror, not onlyconsole— An uncaught exception or an unhandled promise rejection reaches Playwright throughpage.on('pageerror'). It never appears onpage.on('console'): Chromium reports it viaRuntime.exceptionThrown, a different channel fromconsoleAPICalled. Observed cost: a missingconst route = useRoute()in a Nuxt page component threw aReferenceErroron every public page view, killing all client-side analytics and an anchor-jump feature, and it passed a 112-test suite — including the test written specifically to assert the page was clean. The browser console showedUncaught (in promise)the entire time, and nothing else in the chain would have caught it either: a Nuxtcheckchain has no typecheck step,oxlintdoes not flag an undefined identifier, andnuxt builddoes not type-check. Register both listeners, and pair them with an assertion on the effect (the request that should go out, the element that should move) — that one fails for the right reason even when the error channel is wrong. - A test that passes with and without the feature is worse than no test — After a story goes green, disable the mechanism it covers (comment the call, invert the condition) and confirm the test fails. Two cases where this pays: a browser behaviour that masks the implementation (a fragment jump the browser performs anyway, so the spec was pinning Chromium rather than the code), and an error path tested only in its harmless variant (a mail send that rejects returns instantly — the failure that hurts is one that hangs, and only that one needs the timeout assertion).
- Limit local Playwright runs to new + affected specs to keep TDD loops fast — The full Playwright suite is slow and runs in CI. Inside the TDD loop, default to running only the new + affected specs via
lt dev test -- <spec>(lt-projects) orpnpm exec playwright test <spec>(non-lt). Backend Unit + API are fast and stay in the loop unrestricted. Only run the full local Playwright suite when the user explicitly asks.