audit-tautological-tests
MaheshAwasare/claude-skills-pro/skills/audit-tautological-tests/SKILL.md
Find tests that pass without exercising the code under test — over-mocked tests where the mock is the implementation, snapshot tests with no behavioral checks, tests that assert on the mock's return value. Use proactively after a bug ships to "well-tested" code, or when test coverage is high but bug rate is too.
Skill1 starsChanged 5 months ago
What's in it
- Audit Tautological Tests
- When to use
- When NOT to use
- The five test smells
- 1. The mock is the implementation
- 2. Snapshot tests with no behavior contract
- 3. Mocking the system under test
- 4. "Round-trip the mock"
- 5. Tests that import production data
- The mutation test
- Symptoms in your codebase
- What good tests look like
- Anti-patterns
- Verify it worked
---
name: audit-tautological-tests
description: Find tests that pass without exercising the code under test — over-mocked tests where the mock is the implementation, snapshot tests with no behavioral checks, tests that assert on the mock's return value. Use proactively after a bug ships to "well-tested" code, or when test coverage is high but bug rate is too.
---
# Audit Tautological Tests
A tautological test is one where you delete the implementation and the test still passes — because the test is asserting against its own mocks, not against real behavior. These tests give false confidence and let bugs ship.
## When to use
- A bug shipped to code that had "100% coverage."
- High test coverage + high bug rate is a smell.
- Reviewing a PR with many new tests that look suspicious.
- Refactor risk-assessment: which tests would actually catch a regression?
## When NOT to use
- Test suite is genuinely good and bugs are escaping for other reasons (test environment doesn't match prod).
- Looking for slow tests, flaky tests, or coverage gaps — different audits.
## The five test smells
### 1. The mock is the implementation
```ts
// Tautological
test("createUser saves user", async () => {
const mockSave = jest.fn().mockResolvedValue({ id: 1 });
const repo = { save: mockSave };
const result = await createUser(repo, { email: "a@b.com" });
expect(result.id).toBe(1); // asserting on the mock's value
expect(mockSave).toHaveBeenCalled(); // asserting it was called
});
```
What does this test? That `createUser` returns whatever `repo.save` returns. If `createUser`'s body is `return repo.save(...)`, the test passes. If it's `return null` (bug!), the test... still passes? No — the test asserts `result.id === 1`, but the mock returned `{ id: 1 }`, so `result` is `null` and the assertion fails.
OK, this one isn't quite tautological — but it's *close*. It tests pass-through, nothing else.
```ts
// Worse — fully tautological
test("createUser saves user", async () => {
const repo = { save: jest.fn() };
await createUser(repo, { email: "a@b.com" });
expect(repo.save).toHaveBeenCalled();
});
```
Delete the implementation entirely (`async function createUser() {}`). Test fails (`save` not called). OK. Now make the implementation `repo.save({})`. Test passes. The test has no opinion on what gets saved or whether the email is validated.
**Fix:** assert on the *content* of what `save` was called with, and assert the result has the right shape:
```ts
expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({
email: "a@b.com",
createdAt: expect.any(Date),
status: "pending",
}));
```
### 2. Snapshot tests with no behavior contract
```ts
test("renders user card", () => {
const tree = render(<UserCard user={fakeUser} />).toJSON();
expect(tree).toMatchSnapshot();
});
```
Snapshot tests pass as long as the rendered output matches what was *recorded last time*. They catch *unintended* output changes — but only if someone reviews the snapshot diff, which often nobody does ("just run `jest -u` to update").
Snapshot tests for entire components rarely test the thing you care about: that clicking "Save" actually saves.
**Fix:** keep snapshots only for stable UI primitives (icons, brand assets). For interactive components, write behavior tests:
```ts
test("clicking save submits the form", async () => {
const onSave = jest.fn();
render(<UserCard onSave={onSave} />);
await userEvent.click(screen.getByRole("button", { name: /save/i }));
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ email: "a@b.com" }));
});
```
### 3. Mocking the system under test
```ts
test("PaymentService charges customer", () => {
const svc = new PaymentService();
svc.charge = jest.fn().mockResolvedValue({ id: "pay_1" }); // mocked itself!
// ... rest of test
});
```
Or, more subtly:
```ts
jest.spyOn(svc, "validate").mockReturnValue(true); // bypassing the validation
expect(await svc.charge(args)).toEqual({ id: "pay_1" });
```
The test confirms the *non-validate path* works — which is the path that doesn't exist in production. Real payments go through `validate`.
**Fix:** mock at the *boundary* (database, external API), not internal methods of the unit under test.
### 4. "Round-trip the mock"
```ts
test("API returns user", async () => {
fetchMock.get("/api/users/1", { id: 1, email: "a@b.com" });
const user = await client.getUser(1);
expect(user).toEqual({ id: 1, email: "a@b.com" });
});
```
This tests that `client.getUser` makes an HTTP request and returns the JSON. It does *not* test that the API actually returns this shape. Production `/api/users/:id` could return `{ user_id, mail }` and this test would still pass.
**Fix:** if you're mocking the API, you're testing the client's *parsing*. Use a contract test or schema validator that lives near the actual API definition.
### 5. Tests that import production data
```ts
test("calculator sums correctly", () => {
expect(sum(2, 2)).toBe(4);
});
test("calculator with real data", () => {
const inputs = require("./fixtures/yesterday-prod-export.json");
expect(sum(inputs.a, inputs.b)).toBe(inputs.expected);
});
```
The second test "passes" because the fixture was generated from the same code. There's no independent ground truth. If the implementation has the same bug as when the fixture was generated, the test never catches it.
**Fix:** ground truth must come from outside the system — the spec, the user requirement, a hand-computed example.
## The mutation test
The killer test for tautology: **mutate the implementation and see if any test fails.**
- Delete the function body entirely. Run tests. Any pass? Those tests are tautological.
- Replace return value with a constant. Run tests. Any still pass that shouldn't?
- Invert a conditional (`if (x)` → `if (!x)`). Run tests. Any still pass?
Tools like [Stryker](https://stryker-mutator.io) (JS/TS, .NET) or [mutmut](https://github.com/boxed/mutmut) (Python) automate this. Run them. The "mutation score" is the real signal of test quality, not coverage.
## Symptoms in your codebase
```bash
# Find tests that only assert "was called" without arguments
grep -r "toHaveBeenCalled()" tests/ | wc -l
# vs
grep -r "toHaveBeenCalledWith" tests/ | wc -l
```
If the first is much higher than the second, you have a tautology problem.
```bash
# Find snapshot tests
find . -name "__snapshots__" | xargs -I {} ls {} | wc -l
```
How many of these would survive deletion of the component's logic?
## What good tests look like
1. **Test the contract**, not the implementation. "When I call `chargeCustomer` with valid input, a `payments` row is written and a Razorpay order is created" — not "the function calls `db.save` and `razorpay.orders.create` in that order."
2. **Mock at the boundary**, not internally. The DB, the HTTP client, the file system. Not the service's own private methods.
3. **Prefer integration tests** for business logic; reserve unit tests for pure functions and tricky algorithms.
4. **Hand-write fixtures** that represent real shapes, not whatever the impl currently produces.
5. **Aim for mutation score** > 70% on critical code (payments, auth, data).
## Anti-patterns
- **`toHaveBeenCalled()` without `With`** — checks the function ran, not what it did.
- **Mocking everything** — leaves nothing real to test.
- **Snapshot tests as "fast and easy" coverage** — coverage without contract.
- **Mocking the unit under test's own methods** — bypasses the code you're testing.
- **`expect(actual).toEqual(actual)` (with renamed variables)** — yes, this happens.
- **`try { ... } catch { /* test passes */ }`** — error-swallowing tests.
- **Empty assertion blocks** — `test("works", () => { doThing(); });` — no `expect`. Linter rule should catch.
- **Tests with no DB / no HTTP for code that does both** — too pure to be useful.
- **Refusing to delete tautological tests because "coverage will drop"** — coverage was lying.
- **Adding tests after the bug ships, asserting the now-correct behavior, never asserting the wrong behavior wouldn't pass** — write the test before the fix; watch it fail; fix; watch it pass.
## Verify it worked
- [ ] Ran a mutation test (Stryker / mutmut) on critical modules; mutation score > 70%.
- [ ] Found tests where deleting the implementation didn't break them; they're either fixed or deleted.
- [ ] `toHaveBeenCalled()` count is much lower than `toHaveBeenCalledWith()` count.
- [ ] Snapshot tests cover only stable primitives, not interactive components.
- [ ] No test mocks methods of the unit under test.
- [ ] Critical path code (payments, auth) has integration tests against real DB / HTTP, not pure mocks.
- [ ] When a real bug shipped, you can identify which test *should* have caught it; that test is added or the gap is acknowledged.
- [ ] PR review surfaces tautology smells; `toHaveBeenCalled()` without args is a flag.
More agent context in MaheshAwasare/claude-skills-pro
50 other files this repository gives its agents.
Skill
- algolia-searchskills/algolia-search/SKILL.md
- blame-archaeologyskills/blame-archaeology/SKILL.md
- brevo-emailskills/brevo-email/SKILL.md
- bun-runtimeskills/bun-runtime/SKILL.md
- clerk-authskills/clerk-auth/SKILL.md
- cloudflare-workersskills/cloudflare-workers/SKILL.md
- dbt-data-modelingskills/dbt-data-modeling/SKILL.md
- explain-this-diffskills/explain-this-diff/SKILL.md
- extract-skill-from-sessionskills/extract-skill-from-session/SKILL.md
- fastapi-productionskills/fastapi-production/SKILL.md
- find-dead-codeskills/find-dead-code/SKILL.md
- find-real-bugskills/find-real-bug/SKILL.md
- gdpr-dpiaskills/gdpr-dpia/SKILL.md
- github-actions-ciskills/github-actions-ci/SKILL.md
- graphql-relayskills/graphql-relay/SKILL.md
- grpc-servicesskills/grpc-services/SKILL.md
- hipaa-auditskills/hipaa-audit/SKILL.md
- india-dpdp-actskills/india-dpdp-act/SKILL.md
- java-8-to-21skills/java-8-to-21/SKILL.md
- jest-to-vitestskills/jest-to-vitest/SKILL.md
- kubernetes-helmskills/kubernetes-helm/SKILL.md
- mongo-to-postgresskills/mongo-to-postgres/SKILL.md
- nextjs-pages-to-appskills/nextjs-pages-to-app/SKILL.md
- node-version-upgradeskills/node-version-upgrade/SKILL.md
- opentelemetry-instrumentskills/opentelemetry-instrument/SKILL.md
- pci-dss-checklistskills/pci-dss-checklist/SKILL.md
- plan-the-rollbackskills/plan-the-rollback/SKILL.md
- python-2-to-3skills/python-2-to-3/SKILL.md
- razorpay-integrationskills/razorpay-integration/SKILL.md
- react-native-exposkills/react-native-expo/SKILL.md
- scaffold-cli-toolskills/scaffold-cli-tool/SKILL.md
- scaffold-fullstack-appskills/scaffold-fullstack-app/SKILL.md
- scaffold-go-microserviceskills/scaffold-go-microservice/SKILL.md
- scaffold-new-appskills/scaffold-new-app/SKILL.md
- scaffold-saas-starterskills/scaffold-saas-starter/SKILL.md
- sentry-monitoringskills/sentry-monitoring/SKILL.md
- shrink-this-prskills/shrink-this-pr/SKILL.md
- soc2-evidenceskills/soc2-evidence/SKILL.md
- spec-from-conversationskills/spec-from-conversation/SKILL.md
- stripe-integrationskills/stripe-integration/SKILL.md
- supabase-backendskills/supabase-backend/SKILL.md
- terraform-patternsskills/terraform-patterns/SKILL.md
- threat-modeling-strideskills/threat-modeling-stride/SKILL.md
- triage-stack-traceskills/triage-stack-trace/SKILL.md
- wcag-accessibility-auditskills/wcag-accessibility-audit/SKILL.md
- webpack-to-viteskills/webpack-to-vite/SKILL.md
- write-adrskills/write-adr/SKILL.md
- write-commit-messageskills/write-commit-message/SKILL.md
- write-pr-descriptionskills/write-pr-description/SKILL.md
- write-runbookskills/write-runbook/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

