Skip to content
Back to skills

Testing

ASecurity

How to write good tests. Use when writing tests, improving test

  • 19 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
testingjavascripttypescriptgojavaawstestingdebuggingrefactoringapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add TheMostlyGreat/mythos --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/themostlygreat-testing/badge)](https://www.skillsdirectory.com/skills/themostlygreat-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: 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.
user-invocable: false
allowed-tools: "*"
---

# 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.

```text
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

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

| Pattern                     | Problem                                          | Fix                                                                          |
| --------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------- |
| **Coverage theater**        | High line coverage, tests catch no bugs          | Every test should fail if you break the behavior it guards                   |
| **Mock everything**         | Tests only verify mock wiring, not real behavior | Use real dependencies where practical; mock only external services           |
| **Duplicate tests**         | 20 tests with same structure, different values   | Use parameterized/table-driven tests: `it.each(...)`                         |
| **Happy-path only**         | Misses edge cases where real bugs live           | Always include: empty input, boundary values, error paths                    |
| **Hardcoded magic values**  | Timestamps, IDs, paths break across environments | Use builders, relative values, or factories                                  |
| **Snapshot overuse**        | Large snapshots pass review without scrutiny     | Prefer targeted assertions; snapshots only for large stable structures       |
| **Testing private methods** | Couples tests to implementation                  | Test through the public API                                                  |
| **Exact UI text matching**  | Breaks on copy changes                           | Use regex `/submit/i` or data-testid attributes                              |
| **Bug-locking**             | Tests written against buggy code encode the bug  | Write tests BEFORE implementation (TDD), or verify behavior is correct first |
| **Scope defaulting**        | AI defaults to unit tests for everything         | Ask "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.

```typescript
// 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.

```typescript
// 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.

```typescript
// 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.

```yaml
# 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

```typescript
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

```typescript
// 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

```typescript
// 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

| Need                             | Action                                            |
| -------------------------------- | ------------------------------------------------- |
| 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`                                          |

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…