Test-Driven Development

SkillDev tools

Write tests before code; reproduce bugs before fixing them. Use when implementing logic, fixing bugs, or modifying behavior. Tests are proof — "seems right" isn't done.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Test-Driven Development skill

What this skill tells your AI

The instructions your AI receives, as published by ayunis-core/ayunis-core in .claude/skills/test-driven-development/SKILL.md and read by ahel’s review.

Write a failing test before writing the code that makes it pass. For bug fixes, reproduce the bug with a test before attempting a fix. A codebase with good tests is an agent's superpower; a codebase without tests is a liability.

When to use

  • Implementing new logic or behavior
  • Fixing a bug (use the Prove-It pattern below)
  • Modifying existing functionality
  • Adding edge case handling
  • Any change that could break existing behavior

When NOT to use: pure config changes, doc updates, static content with no behavioral impact.

The cycle: Red → Green → Refactor

  1. Red — Write a test that fails. A test that passes immediately proves nothing.
  2. Green — Write the minimum code to make it pass. Don't over-engineer.
  3. Refactor — Clean up with tests still green. Run tests after every refactor step.

Repeat. One concept at a time.

The Prove-It pattern (bug fixes)

When a bug is reported, don't start by trying to fix it. Start by writing a test that reproduces it.

  1. Write a test that demonstrates the bug. It must fail.
  2. The failing test confirms the bug exists and that you understand it.
  3. Implement the fix.
  4. The test now passes — fix proven, regression guarded.
  5. Run the additional validation required by the repository's Proportional Workflow and the applicable surface-specific skill.

If you can't write a failing test for the bug, you don't understand the bug yet. Stop and figure it out before touching the code.

The test pyramid

Distribute testing effort by level:

        ╱╲
       ╱E2E╲          ~5% — full user flows, real browser
      ╱─────╲
     ╱integ. ╲        ~15% — boundaries, API, DB
    ╱────────╲
   ╱   unit   ╲       ~80% — pure logic, isolated, ms each
  ╱────────────╲

The Beyonce Rule: if you liked it, you should have put a test on it. Refactors don't catch your bugs — your tests do. Untested code that breaks is on you.

Test sizes

Classify by what the test consumes:

SizeConstraintsSpeedWhere
SmallSingle process, no I/O, no DB, no networkmsPure logic, transforms, validators
MediumLocalhost only, no external servicessecondsAPI + test DB, components
LargeExternal services allowedminutesE2E, perf, staging integration

The vast majority of the suite should be small. They're fast, reliable, and easy to debug.

Writing good tests

Test state, not interactions

Assert on the outcome of the operation, not on which methods were called internally. Interaction-based tests break on every refactor even when behavior is unchanged.

// Good — tests behavior
const tasks = await listTasks({ sortBy: "createdAt", sortOrder: "desc" });
expect(tasks[0].createdAt.getTime()).toBeGreaterThan(
  tasks[1].createdAt.getTime(),
);

// Bad — tests SQL string
expect(db.query).toHaveBeenCalledWith(
  expect.stringContaining("ORDER BY created_at DESC"),
);

DAMP over DRY in tests

In production code, DRY is usually right. In tests, DAMP (Descriptive And Meaningful Phrases) wins. Each test should read like a specification — self-contained, no tracing through shared helpers to figure out what's being verified. Duplication in tests is acceptable when it makes each test independently understandable.

Prefer real implementations

Use the simplest test double that gets the job done. The closer to real, the more confidence:

Real implementation > Fake (in-memory) > Stub (canned data) > Mock (interaction)

Mock only when the real implementation is too slow, non-deterministic, or has side effects you can't control (external APIs, email sending). Over-mocking creates tests that pass while production breaks.

Arrange-Act-Assert

it("marks tasks overdue when deadline has passed", () => {
  // Arrange
  const task = createTask({ title: "Test", deadline: new Date("2025-01-01") });
  // Act
  const result = checkOverdue(task, new Date("2025-01-02"));
  // Assert
  expect(result.isOverdue).toBe(true);
});

One assertion per concept

Each test verifies one behavior. Names read like a spec.

// Good
it('rejects empty titles', ...);
it('trims whitespace from titles', ...);
it('enforces maximum title length', ...);

// Bad
it('validates titles correctly', () => {
  // ten different things in one test
});

Anti-patterns

Anti-patternWhy it hurtsFix
Testing implementation detailsTests break on every refactorTest inputs and outputs
Flaky tests (timing, ordering)Erodes trust in the suiteDeterministic assertions, isolated state
Testing framework codeWastes effortOnly test YOUR code
Snapshot abuseNobody reviews, breaks on any changeUse sparingly, review every change
No test isolationPass alone, fail togetherPer-test setup/teardown
Mocking the unit under testTests prove nothing about real behaviorMock dependencies, never the subject

Common rationalizations

RationalizationReality
"I'll write tests after the code works."You won't. And post-hoc tests test the implementation you wrote, not the behavior you needed.
"This is too simple to test."Simple code gets complicated. The test documents the expected behavior for the next person.
"Tests slow me down."Tests slow you down now. They speed you up every time you change the code later.
"I tested it manually."Manual testing doesn't persist. Tomorrow's refactor breaks it with no way to know.
"The code is self-explanatory."Tests are the spec. They describe what the code should do, not what it currently does.
"It's just a prototype."Prototypes become production. Tests from day one avoid the test-debt crisis.
"I'll add the bug-reproduction test after I've fixed it."If you fix first, the test only verifies your assumption about the fix, not the original bug. The test must fail before the fix.
"All tests pass."Did you actually run them? "All tests pass" with no command output is the agent equivalent of "trust me." Show the output.
"This test is too hard to write — let me skip it."Hard-to-test code is a design smell. The friction is signal, not noise. Restructure the code so it can be tested.

Red flags

  • Writing code without a corresponding test
  • Tests that pass on the first run (verify they actually fail when you break the code under test)
  • Bug fixes without a reproduction test that failed before the fix
  • Test names that don't describe a behavior ("works", "handles errors", "test 3")
  • Skipping or disabling tests to make the suite pass
  • Mocking the unit under test
  • "All tests pass" without producing the test runner's output

Verification

After implementation:

  • Every new behavior has a corresponding test
  • All tests required by the Proportional Workflow pass — and the test runner's output proves it
  • Bug fixes include a reproduction test that failed before the fix
  • Test names describe behavior, not implementation
  • No skipped or disabled tests
  • Coverage hasn't decreased (if tracked)

Signals

GitHub stars
33
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
test-driven-development-ayunis-core
Source
github.com/ayunis-core/ayunis-core