# Improving Weak Tests

> Rewrite a weak unit spec into one that kills mutations. Use when you must fix a toBeTruthy/toBeDefined/toBeFalsy assertion, a bare mock-fired check, an array-length-only check, a shapeless snapshot, or a private-state peek. Triggers on "rewrite this spec", "stronger assertions", "catch mutations".

- Skill: `ancplua/improving-weak-tests` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ancplua/improving-weak-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ancplua/improving-weak-tests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ANcpLua (https://skillmd.com/u/ancplua)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/ancplua/improving-weak-tests

---


# Improving Weak Tests

## Core principle

Every rewrite must answer: **what plausible mutation does the new test catch
that the old one did not?** If you cannot name that mutation in one
sentence, the rewrite is not done.

The catalogue below is written in TypeScript against jest/vitest matchers
(`toEqual`, `toHaveBeenCalledWith`, `expect.any`), but the weak-assertion
patterns are language-agnostic — the same rewrites apply to pytest in Python
(see the pytest note under rule 1).

## The catalogue

### 1. `toBeTruthy()` → exact shape

```ts
// WEAK
expect(await saveUser(input)).toBeTruthy();

// STRONG
const result = await saveUser(validInput);
expect(result).toEqual({
  id: expect.any(String),
  email: validInput.email,
  createdAt: expect.any(Date),
});
```

Catches: `saveUser` returning `{}` or echoing the input.

The same weakness in pytest is a bare truthy `assert`; rewrite it to pin the
exact shape:

```python
# WEAK
assert save_user(valid_input)

# STRONG
from datetime import datetime

result = save_user(valid_input)
assert result.email == valid_input.email
assert isinstance(result.id, str)
assert isinstance(result.created_at, datetime)
```

### 2. "mock was called" → state + args + output

```ts
// WEAK
await vm.saveUser(input);
expect(httpClient.post).toHaveBeenCalled();

// STRONG
await vm.saveUser(input);
expect(httpClient.post).toHaveBeenCalledWith(`${baseUrl}/users`, input);
expect(vm.users()).toContainEqual(expect.objectContaining({ email: input.email }));
expect(vm.isFormVisible()).toBe(false);
expect(vm.errorMessage()).toBeNull();
```

Catches: the handler not reloading, not closing the form, or silently
failing.

### 3. Happy path only → add failure path + negative space

```ts
it('keeps form open and exposes error when create fails', async () => {
  vm.openCreateForm();
  const promise = vm.saveUser(validInput);
  httpTesting.expectOne('POST', `${baseUrl}/users`)
    .error(new ProgressEvent('error'));
  await promise;
  expect(vm.isFormVisible()).toBe(true);
  expect(vm.errorMessage()).toMatch(/create failed|error/i);
  expect(vm.isSaving()).toBe(false);
  expect(vm.users()).toEqual([]);
});

it('does not issue a write when input is invalid', async () => {
  const promise = vm.saveUser(invalidInput);
  await promise;
  httpTesting.expectNone('POST', `${baseUrl}/users`);
  expect(vm.errorMessage()).toMatch(/invalid/i);
});
```

Catches: silent error swallowing and missing input validation.

### 4. `array.length === N` → content + order

```ts
// WEAK
expect(vm.users().length).toBe(1);

// STRONG
expect(vm.users()).toEqual([
  expect.objectContaining({ id: createdUser.id, email: createdUser.email }),
]);
```

Catches: returning `[anything]` instead of the real user.

### 5. Implementation peek → public contract

If a test reads `_internalMap` or calls `resetForTesting()`, rewrite against
a public observable outcome. If no public outcome exists, the behavior has
no observable contract — hand to `reviewing-testability` rather than keep
the peek.

### 6. Empty snapshot → hand-crafted structural assertion

```ts
// WEAK
expect(vm.state()).toMatchSnapshot();

// STRONG
expect(vm.state()).toEqual({
  phase: 'loaded',
  users: expect.arrayContaining([
    expect.objectContaining({ id: createdUser.id }),
  ]),
  selectedUserId: createdUser.id,
  error: null,
});
```

### 7. Timeout waits → condition-based waiting

```ts
// WEAK
await new Promise(r => setTimeout(r, 100));
expect(vm.loaded()).toBe(true);

// STRONG
await waitFor(() => vm.loaded() === true);
expect(vm.users()).toEqual(expectedUsers);
```

### 8. State transition: before → during → after

```ts
expect(vm.isSaving()).toBe(false);
const p = vm.saveUser(validInput);
expect(vm.isSaving()).toBe(true);
// flush HTTP
await p;
expect(vm.isSaving()).toBe(false);
expect(vm.users()).toContainEqual(
  expect.objectContaining({ email: validInput.email }),
);
```

Catches: the saving flag never being set, or never being cleared.

## Hard rules

- Never use `toBeTruthy()`, `toBeDefined()`, `toBeFalsy()` as a primary
  assertion.
- Never rely on `toHaveBeenCalled()` alone — pair with
  `toHaveBeenCalledWith(specific)` and a state or output assertion, or drop
  the mock check.
- Never assert `.length` without content.
- Never leave a behavior with only a positive-path test.
- Never write `expect(true).toBe(true)` or equivalent padding.
- If you cannot assert something specific because the code shape prevents
  it, stop. Hand the case to `reviewing-testability` rather than writing a
  compromise test.

## Output

For each rewrite, one diff block:

```markdown
### <file>:<test name>

**Before (WEAK)**
<old body>

**After (STRONG)**
<new body>

**Mutation caught:** one sentence.
```

Ship the negative-space counterpart in the same unit of work when the
original covered only the happy path.

## Related

- Before rewriting: run `judging-test-quality` to confirm the verdict.
- If rewriting keeps hitting architectural walls: `reviewing-testability`.
- When driving a whole file or module to full coverage:
  `mutation-resistant-coverage`.

