본문으로 건너뛰기
cccskills
무료GitHub에 공개

testing

How to write good tests. Use when writing tests, improving test coverage, or evaluating test quality. Also invoked by other skills — BDD at RED phase, tdd-review at GREEN gate, refactor at PROTECT phase, and debug. Core test quality knowledge across all workflows.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md10.4 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Writing Good Tests

Tests prove the system behaves correctly. Every test — unit, integration, E2E, eval — must verify observable behavior, not implementation details.

Core Principle: TEST BEHAVIOR, NOT IMPLEMENTATION


Philosophy: Behavior-Biased Testing

What this means: At every test level, assert on what the system does (outputs, side effects, user-visible outcomes) — never on how it does it (internal state, mock call counts, private methods).

Why: Tests coupled to implementation break on every refactor. Behavioral tests survive refactoring because behavior doesn't change — only the internals do.

Scope preference: When multiple test types can verify a behavior, prefer the highest scope that covers the behavior with acceptable feedback speed. Higher scope = more confidence that the real system works.

Prefer (highest confidence):
  E2E        → proves the user can do the thing
  Integration → proves components work together
  Unit        → proves the algorithm is correct
Fallback (lowest scope):

When to drop to a lower scope:

  • Pure function with many edge cases (20+ combinations) → unit test
  • Internal service boundary, no UI involved → integration test
  • Algorithm with complex logic (parsing, math, state machines) → unit test
  • Only one module's contract matters → integration test

When to stay at higher scope:

  • User-facing feature or workflow → E2E
  • Multiple modules must cooperate → integration or E2E
  • "If this breaks, users notice immediately" → E2E

Announce your decision: "Test type: [unit/integration/E2E/eval] because [reason]."

For the full decision tree, bug detection matrix, and edge cases: .safeword/guides/testing-guide.md


Iron Laws

Non-negotiable at every test level. Violating these produces tests that pass but catch nothing.

1. Test Behavior, Not Implementation

// WRONG — tests internal state
expect(component.state.count).toBe(1);
expect(mockFn).toHaveBeenCalledWith("internal-detail");

// RIGHT — tests observable behavior
expect(screen.getByText("Count: 1")).toBeVisible();
expect(result).toEqual({ total: 42 });

This applies at EVERY level:

  • Unit: assert on return values, not on which helpers were called
  • Integration: assert on API responses, not on which service methods fired
  • E2E: assert on what the user sees, not on DOM structure
  • Eval: grade the output quality, not the path the LLM took

2. Every Test Needs a Meaningful Assertion

If your assertion would pass for ANY input, it asserts nothing.

// WRONG — asserts nothing useful
expect(() => processData(input)).not.toThrow();
expect(result).toBeTruthy();
expect(result).toBeDefined();

// RIGHT — asserts specific behavior
expect(processData(input)).toEqual({ status: "ok", count: 3 });
expect(result.errors).toHaveLength(0);

3. Tests Must Fail First

A new test that passes immediately is testing nothing — or testing something that already works (no value added). For new behavior: RED then GREEN. For existing code: if a characterization test fails, you found a bug.

4. One Test, One Behavior

If a test name has "and" in it, split it. Each test verifies ONE observable outcome.

// WRONG
it('validates input and saves to database', ...);

// RIGHT
it('rejects input missing required field', ...);
it('saves valid input to database', ...);

5. Tests Must Be Independent

No test depends on another test's side effects. Fresh state per test. Run in any order.


Anti-Patterns

The most common ways AI-generated tests go wrong. Watch for all of them.

PatternProblemFix
Coverage theaterHigh line coverage, tests catch no bugsEvery test should fail if you break the behavior it guards
Mock everythingTests only verify mock wiring, not real behaviorUse real dependencies where practical; mock only external services
Duplicate tests20 tests with same structure, different valuesUse parameterized/table-driven tests: it.each(...)
Happy-path onlyMisses edge cases where real bugs liveAlways include: empty input, boundary values, error paths
Hardcoded magic valuesTimestamps, IDs, paths break across environmentsUse builders, relative values, or factories
Snapshot overuseLarge snapshots pass review without scrutinyPrefer targeted assertions; snapshots only for large stable structures
Testing private methodsCouples tests to implementationTest through the public API
Exact UI text matchingBreaks on copy changesUse regex /submit/i or data-testid attributes
Bug-lockingTests written against buggy code encode the bugWrite tests BEFORE implementation (TDD), or verify behavior is correct first
Scope defaultingAI defaults to unit tests for everythingAsk "what's the highest scope with acceptable feedback speed?" first

Behavioral Testing by Type

Unit Tests — Behavioral

Test the contract (inputs → outputs), not the internals.

// Behavioral: asserts on output
it("applies 20% discount for VIP users", () => {
  expect(calculateDiscount(100, { tier: "VIP" })).toBe(80);
});

// Non-behavioral: asserts on internal call
it("calls applyRate with 0.2", () => {
  calculateDiscount(100, { tier: "VIP" });
  expect(applyRate).toHaveBeenCalledWith(0.2);
});

Integration Tests — Behavioral

Test that components produce correct combined outcomes with real dependencies.

// Behavioral: asserts on combined outcome
it('returns user profile with computed permissions', async () => {
  const response = await api.get('/users/1/profile');
  expect(response.data.permissions).toContain('edit_posts');
});

// Non-behavioral: asserts on which services were called
it('calls UserService then PermissionService', async () => { ... });

E2E Tests — Behavioral

Test what the user can see and do. E2E tests are naturally behavioral — lean into this.

// Behavioral: user-visible outcome
test("user creates account and sees dashboard", async ({ page }) => {
  await page.goto("/signup");
  await page.fill('[name="email"]', "test@example.com");
  await page.fill('[name="password"]', "secure123");
  await page.click('button:has-text("Sign Up")');
  await expect(page).toHaveURL("/dashboard");
  await expect(page.getByText("Welcome")).toBeVisible();
});

LLM Evals — Behavioral

Grade what the output achieves, not the path the model took. Use deterministic assertions first, LLM-as-judge second.

# Deterministic assertion (cheap, run every commit)
- type: javascript
  value: JSON.parse(output).intent === 'order_pizza'

# LLM-as-judge (for subjective quality, run on PR/schedule)
- type: llm-rubric
  value: |
    PASS: Correctly identifies pizza order, confirms size and type
    FAIL: Wrong intent, ignores key details, or generic response

Eval-specific principles:

  • Grade outcomes, not paths — the LLM can take any route to the right answer
  • Binary PASS/FAIL over scales — "3 vs 4" is meaningless; force clarity
  • One dimension per scorer — don't bundle factuality + tone + completeness
  • Deterministic checks first — regex, schema validation, required fields before LLM-as-judge

Writing Approach

Match Existing Style

Before writing any test, find existing tests near the code under test. Match their imports, describe/it structure, helpers, and patterns. Don't introduce new conventions into an established test suite.

If no existing tests: use AAA pattern (Arrange-Act-Assert).

Design Before Writing

List planned tests before coding. For each test, name:

  • What behavior it verifies (not what code it calls)
  • What the key assertion is (not "it doesn't throw")
  • Why this test matters (what bug would slip through without it?)

Aim for: happy path + edge cases + error cases + at least one test the implementation could plausibly get wrong.

One Test at a Time

Write one test → run it → verify it fails (or passes for characterization) → move to next. Never write all tests at once then run them.


Patterns

Test Data Builders

function buildUser(overrides = {}) {
  return { id: "test-1", name: "Test User", role: "member", ...overrides };
}

it("applies VIP discount", () => {
  const user = buildUser({ role: "vip" });
  expect(calculateDiscount(user)).toBe(0.2);
});

Async Testing — Never Use Arbitrary Timeouts

// WRONG
await sleep(3000);
await page.waitForTimeout(500);

// RIGHT — wait for condition
await expect.poll(() => getStatus()).toBe("ready");
await waitFor(() => expect(element).toBeVisible());

Descriptive Test Names

// WRONG
it("works correctly");
it("should handle edge case");

// RIGHT — describes the behavior
it("returns 401 when API key is missing");
it("preserves user input after validation error");

Quick Reference

NeedAction
Full test type selection guide.safeword/guides/testing-guide.md
Test definition template (BDD).safeword/templates/test-definitions-feature.md
Test quality review/audit
Feature-level TDD with scenarios/bdd
Debugging failing tests/debug

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

audit

무료

Run comprehensive code audit for architecture, dead code, and test quality. Use when reviewing overall codebase health, checking for architectural violations, or before marking a feature complete.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

bdd

무료

Behavior-first feature development — use when building new capabilities, continuing feature work, or when work introduces new state or multiple user flows. Discovers desired behavior through examples and scenarios before implementation. Do NOT use for bug fixes, typos, or small isolated changes.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

Use when the user wants to explore options, weigh approaches, or think through uncertainty before committing to a direction. Collaborative brainstorming and rubber ducking — divergence-first thinking partner.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

Kill zombie dev servers and test processes. Use when ports are blocked, processes are hanging, or test runners won't start.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

debug

무료

Root cause debugging before fixes. Use when investigating bugs, diagnosing test failures, troubleshooting unexpected behavior, or when previous fix attempts failed. Enforces investigate-first discipline.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

elicit

무료

Extract tacit knowledge through non-obvious microquestions — things only the user knows that can't be found in code, docs, or research. Use when you're about to guess at intent, context, or constraints during SAFEWORD's understanding flow. Also use when user says 'ask me', 'what do you need to know', or when another skill (bdd, brainstorm, debug) needs user context before proceeding. Do NOT use for questions answerable by reading the codebase or searching the web.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

TheMostlyGreat/mythos192026년 6월 14일 업데이트

TheMostlyGreat의 스킬 모두 보기

이 스킬의 문제 신고