Claude
Skills
Sign in
← Back

writing-tests

Included with Lifetime
$97 forever

Principles for writing effective, maintainable tests. Covers naming conventions, assertion best practices, and comprehensive edge case checklists. Based on BugMagnet by Gojko Adzic.

Writing & Docs

What this skill does


# Writing Tests

How to write tests that catch bugs, document behavior, and remain maintainable.

> Based on [BugMagnet](https://github.com/gojko/bugmagnet-ai-assistant) by Gojko Adzic. Adapted with attribution and aligned with fn(args, deps) and Result type patterns.

## Core Principle

Test names describe outcomes, not actions. Assertions match test titles. Specific assertions catch specific bugs. One concept per test.

## Critical Rules

🚨 **Test names describe outcomes, not actions.** "returns empty array when input is null" not "test null input". The name IS the specification.

🚨 **Assertions must match test titles.** If the test claims to verify "different IDs", assert on the actual ID valuesβ€”not just count or existence.

🚨 **Assert specific values, not types.** `expect(result.value).toEqual(['First.', ' Second.'])` not `expect(result).toBeDefined()`. Specific assertions catch specific bugs.

🚨 **One concept per test.** Each test verifies one behavior. If you need "and" in your test name, split it.

🚨 **Bugs cluster together.** When you find one bug, test related scenarios. The same misunderstanding often causes multiple failures.

## When This Applies

- Writing new tests (especially during TDD RED phase)
- Reviewing test quality
- Expanding test coverage
- Investigating discovered bugs
- Refactoring tests to be more maintainable

## Test Naming

**Pattern:** `[outcome] when [condition]`

### Good Names (Describe Outcomes)

```typescript
// βœ… GOOD: Describes what happens
it('returns empty array when input is null', () => {});
it('returns err NOT_FOUND when user does not exist', () => {});
it('calculates tax correctly for tax-exempt items', () => {});
it('preserves original order when duplicates removed', () => {});
it('returns ok with user when found in database', () => {});
```

### Bad Names (Describe Actions)

```typescript
// ❌ BAD: Describes what you're doing, not what happens
it('test null input', () => {});           // What about null input?
it('should work', () => {});                // What does "work" mean?
it('handles edge cases', () => {});         // Which edge cases?
it('email validation test', () => {});      // What's being validated?
it('test getUser', () => {});               // What does getUser do?
```

### The Specification Test

Your test name should read like a specification. If someone reads ONLY the test names, they should understand the complete behavior of the system.

```typescript
describe('getUser', () => {
  it('returns ok with user when found in database', () => {});
  it('returns err NOT_FOUND when user does not exist', () => {});
  it('returns err DB_ERROR when database connection fails', () => {});
  it('returns err VALIDATION_ERROR when userId is empty string', () => {});
});

// Reading just these names tells you everything getUser does.
```

## Assertion Best Practices

### Assert Specific Values

```typescript
// ❌ WEAK - passes even if completely wrong data
it('returns user when found', async () => {
  const result = await getUser({ userId: '123' }, deps);
  expect(result).toBeDefined();
  expect(result.ok).toBeTruthy();
  if (result.ok) {
    expect(result.value).toBeTruthy();
  }
});

// βœ… STRONG - catches actual bugs
it('returns ok with user when found in database', async () => {
  const mockUser = { id: '123', name: 'Alice', email: '[email protected]' };
  const deps = mock<GetUserDeps>();
  deps.db.findUser.mockResolvedValue(mockUser);

  const result = await getUser({ userId: '123' }, deps);

  expect(result.ok).toBe(true);
  if (result.ok) {
    expect(result.value).toEqual(mockUser);
    expect(result.value.email).toBe('[email protected]');
  }
});
```

### Match Assertions to Test Title

```typescript
// ❌ TEST SAYS "different IDs" BUT ASSERTS COUNT
it('generates different IDs for each call', () => {
  const id1 = generateId();
  const id2 = generateId();
  expect([id1, id2]).toHaveLength(2);  // WRONG: doesn't check they're different!
});

// βœ… ACTUALLY VERIFIES DIFFERENT IDs
it('generates different IDs for each call', () => {
  const id1 = generateId();
  const id2 = generateId();
  expect(id1).not.toBe(id2);  // RIGHT: verifies the claim
  expect(id1).toMatch(/^[a-z0-9-]+$/);  // Also verify format
  expect(id2).toMatch(/^[a-z0-9-]+$/);
});
```

### Testing Result Types

When testing functions that return `Result<T, E>`, assert on the specific error type and value:

```typescript
// ❌ WEAK - doesn't verify error type
it('handles missing user', async () => {
  const result = await getUser({ userId: 'missing' }, deps);
  expect(result.ok).toBe(false);
});

// βœ… STRONG - verifies exact error
it('returns err NOT_FOUND when user does not exist', async () => {
  const deps = mock<GetUserDeps>();
  deps.db.findUser.mockResolvedValue(null);

  const result = await getUser({ userId: 'missing' }, deps);

  expect(result.ok).toBe(false);
  if (!result.ok) {
    expect(result.error).toBe('NOT_FOUND');
  }
});
```

### Avoid Implementation Coupling

```typescript
// ❌ BRITTLE - tests implementation details
it('queries database with correct SQL', async () => {
  const deps = mock<GetUserDeps>();
  await getUser({ userId: '123' }, deps);
  expect(deps.db.findUser).toHaveBeenCalledWith('123');
  // What if we change to findUser({ where: { id: '123' } })? Test breaks.
});

// βœ… FLEXIBLE - tests behavior
it('returns ok with user when found in database', async () => {
  const mockUser = { id: '123', name: 'Alice' };
  const deps = mock<GetUserDeps>();
  deps.db.findUser.mockResolvedValue(mockUser);

  const result = await getUser({ userId: '123' }, deps);

  expect(result.ok).toBe(true);
  if (result.ok) {
    expect(result.value).toEqual(mockUser);
  }
  // Implementation can change (SQL, ORM, etc.) and test still passes
});
```

## Test Structure

### Arrange-Act-Assert

```typescript
it('calculates total with tax for non-exempt items', () => {
  // Arrange: Set up test data and mocks
  const item = { price: 100, taxExempt: false };
  const taxRate = 0.1;
  const deps = mock<CalculateTotalDeps>();
  deps.config.getTaxRate.mockReturnValue(taxRate);

  // Act: Execute the behavior
  const result = calculateTotal({ item }, deps);

  // Assert: Verify the outcome
  expect(result.ok).toBe(true);
  if (result.ok) {
    expect(result.value).toBe(110);
  }
});
```

### One Concept Per Test

```typescript
// ❌ MULTIPLE CONCEPTS - hard to diagnose failures
it('validates and processes order', () => {
  expect(validate(order)).toBe(true);
  expect(process(order).ok).toBe(true);
  if (process(order).ok) {
    expect(sendEmail).toHaveBeenCalled();
  }
});

// βœ… SINGLE CONCEPT - clear failures
it('accepts valid orders', () => {
  const result = validate(validOrder);
  expect(result).toBe(true);
});

it('rejects orders with negative quantities', () => {
  const result = validate(negativeQuantityOrder);
  expect(result).toBe(false);
});

it('sends confirmation email after processing', async () => {
  const deps = mock<ProcessOrderDeps>();
  deps.mailer.send.mockResolvedValue(undefined);
  
  const result = await processOrder(validOrder, deps);
  
  expect(result.ok).toBe(true);
  expect(deps.mailer.send).toHaveBeenCalledWith(validOrder.customerEmail);
});
```

## Edge Case Checklists

When testing a function, systematically consider these edge cases based on input types.

### Numbers

- [ ] Zero
- [ ] Negative numbers
- [ ] Very large numbers (near MAX_SAFE_INTEGER)
- [ ] Very small numbers (near MIN_SAFE_INTEGER)
- [ ] Decimal precision (0.1 + 0.2)
- [ ] NaN
- [ ] Infinity / -Infinity
- [ ] Boundary values (off-by-one at limits)

```typescript
describe('calculateTotal', () => {
  it('returns err INVALID when price is negative', () => {});
  it('returns err INVALID when price is zero', () => {});
  it('returns err INVALID when price is NaN', () => {});
  it('handles very large prices near MAX_SAFE_INTEGER', () => {});
  it('handles decimal precision correctly', () => {
    // 0.1 + 0.2 = 0.30000000000000004
  });
});
```

Related in Writing & Docs