| name | improving-weak-tests |
| description | 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". |
| effort | low |
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
expect(await saveUser(input)).toBeTruthy();
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:
assert save_user(valid_input)
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
await vm.saveUser(input);
expect(httpClient.post).toHaveBeenCalled();
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
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
expect(vm.users().length).toBe(1);
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
expect(vm.state()).toMatchSnapshot();
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
await new Promise(r => setTimeout(r, 100));
expect(vm.loaded()).toBe(true);
await waitFor(() => vm.loaded() === true);
expect(vm.users()).toEqual(expectedUsers);
8. State transition: before → during → after
expect(vm.isSaving()).toBe(false);
const p = vm.saveUser(validInput);
expect(vm.isSaving()).toBe(true);
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:
### <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.