skills/test-writer/SKILL.md
Write, extend, or review tests in any codebase. Use this skill whenever the user asks to write tests, add test coverage, test a new feature, fix failing tests, or audit existing test files — regardless of language, framework, or project. Also trigger for "add tests for", "write tests for", "cover this with tests", "test this file", "update the tests", "improve coverage", or "this needs tests". This skill enforces universal testing rules (no .skip, no lowering thresholds, full-path coverage) and adapts its mock patterns and tooling to whatever stack the repo uses.
npx skillsauth add anchildress1/awesome-github-copilot test-writerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are writing production-quality tests. These rules apply to every repo, every language, every test framework.
Do ALL of the following before writing a single line of test code — in parallel where possible:
package.json scripts.test, Makefile, pyproject.toml, or the repo's CI config. You will need this for verification.tests/ or __tests__/ directory; if none, create one mirroring the source structure.vitest.config.ts, jest.config.ts, pytest.ini, pyproject.toml). Note coverage thresholds and excluded paths.CLAUDE.md or AGENTS.md for project-specific test conventions.Detect the framework from config files before writing anything:
| Signal | Framework |
| - | - |
| vitest.config.ts or vitest in package.json | Vitest |
| jest.config.* or jest in package.json | Jest |
| pytest.ini, pyproject.toml [tool.pytest] | pytest |
| .mocharc.* | Mocha |
| cypress.config.* | Cypress (e2e) |
| playwright.config.* | Playwright (e2e) |
File placement — match the repo's existing convention:
src/foo.ts → src/foo.test.ts), follow that.tests/foo_test.py), follow that.tests/ for integration and e2e.Naming — match what already exists:
<module>.test.ts or <module>.spec.tstest_<module>.py (standard pytest convention)These apply in every repo. They exist because deviations silently erode test suite confidence.
No .skip. Skipped tests are lies the suite tells future readers. If a test fails, diagnose the real cause — in source, in the mock, or in the test logic — and fix it.
No lowering coverage thresholds. If coverage drops, the answer is more tests. The threshold in the config is a floor.
No copy-paste test bodies. Shared setup belongs in beforeEach (or equivalent). Tests that differ only in input/output belong in parametric form (it.each, @pytest.mark.parametrize).
Assert both result shape and mock interactions. Checking only the return value misses downstream call correctness. Checking only mock calls misses what the caller actually produces. Do both.
Tests must be self-contained. No test should depend on execution order or mutable state that beforeEach does not reset. A test that passes in isolation but fails in a suite is a broken test.
Fix the source, not the assertion. If you discover a bug while writing tests, fix it in the source code first, then write the test against correct behavior. Adjusting the assertion to match wrong behavior documents a bug instead of catching it.
Name tests so a reader who has never seen the source understands what is verified and under what condition — without reading the body.
it('returns 404 when the card does not exist')
it('normalizes whitespace-only url to undefined')
it('rejects signal above 5')
it('excludes soft-deleted cards from results by default')
it('includes soft-deleted cards when include_deleted is true')
it('handles partial batch failure without failing the whole run')
Pattern: verb + subject + condition. Avoid it('works'), it('test 1'), it('should be correct').
Group related tests under describe blocks that encode shared context:
describe('when input is valid')
describe('when the database returns an error')
describe('when deleted_at is provided')
Every test follows this shape. Make sections visually obvious with blank lines in longer tests:
it('creates a card and returns the persisted record', async () => {
// Arrange
const input = buildValidCard({ title: 'My Note' });
mockDb.upsert.mockResolvedValue({ data: input, error: null });
// Act
const result = await handleWriteCards(supabase, [input]);
const body = asTextJson(result);
// Assert
expect(body.written).toBe(1);
expect(body.results[0].title).toBe('My Note');
expect(mockDb.upsert).toHaveBeenCalledWith(
expect.objectContaining({ title: 'My Note' }),
expect.anything(),
);
});
For every function, method, handler, or route you test, work through this in order. "Obvious" paths are where regressions hide — do not skip a row.
| Path | What to test |
| - | - |
| Happy path | Valid input, all required fields present, dependencies succeed. Assert full response shape and all mock call arguments. |
| Negative / validation | Missing required fields (each one), wrong types, out-of-range values, whitespace-only strings, empty collections, oversized collections, invalid formats (UUID, URL, email, datetime). |
| Error / dependency failure | The DB/API/service returns an error object. The HTTP call fails. The file does not exist. Assert the caller surfaces the error correctly — right status code, right message, isError flag if applicable. |
| Exception / unexpected throw | The dependency throws synchronously or rejects unexpectedly. Assert the outer error handler catches it and returns a safe, structured response. |
| Edge cases | null and undefined for optional fields. Empty arrays. Boundary values (min, max, exactly at limit, one over). Whitespace normalization. Timezone offsets. Circular references if the code serializes. State transitions (e.g., soft-delete on/off, flag enabled/disabled). |
Practical rule: if you can construct an input that takes a different code path, it needs a test.
For every schema or validator, test every field:
number where string expected, string where boolean expected.undefined, timezone offset → UTC ISO string.Use the framework's parametric API instead of repeating test bodies:
// Vitest / Jest
it.each([
{ label: 'null', value: null },
{ label: 'undefined', value: undefined },
{ label: 'whitespace only', value: ' ' },
])('normalizes $label url to undefined', ({ value }) => {
expect(schema.parse({ ...valid, url: value }).url).toBeUndefined();
});
# pytest
@pytest.mark.parametrize("value,expected", [
(None, None),
("", None),
(" ", None),
])
def test_url_normalization(value, expected):
assert schema.parse({"url": value}).url == expected
Mock at the process boundary, not inside the logic.
| What to mock | Why |
| - | - |
| Network clients (HTTP, DB SDKs, message queues) | Prevent real I/O, control response shapes |
| Date.now(), Math.random(), clocks | Determinism |
| External service SDKs | Speed and isolation |
| Internal helpers within the same module | Do not — test through the real call chain |
| Logging side effects (when asserting them) | Spy, not replace — restore after the test |
Unit tests: mock the dependency, test the logic in isolation.
Integration tests: let real middleware, validation, routing, and business logic run. Mock only at the true process boundary. Test through the HTTP layer, not by calling handlers directly.
End-to-end tests: no mocks. Real services, real data, real cleanup.
Adapt to the repo's existing mock style. Consistency beats preference. If the repo uses factory functions with _mocks exposed for assertion, follow that. If it uses vi.mock / jest.mock, follow that. If it uses constructor injection with fakes, follow that.
Reset mocks in beforeEach. Use the framework-appropriate method:
vi.clearAllMocks() or vi.resetAllMocks() (reset also clears return values and implementations)jest.clearAllMocks() or jest.resetAllMocks()monkeypatch fixture auto-resets; for unittest.mock, use patch as a context manager or with autospecWrite performance tests for any code path with latency or throughput requirements:
bench, pytest-benchmark, k6, or equivalent.// Vitest bench example
bench('processes 1000 cards under 50ms', async () => {
await processCards(generateCards(1000));
}, { time: 500 });
Write LHCI tests for any user-facing page or route. Do not wire them into GHA CI — they run locally only.
lighthouserc.js or lighthouserc.json).// lighthouserc.js example
module.exports = {
ci: {
assert: {
assertions: {
'categories:performance': ['warn', { minScore: 0.8 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
},
},
},
};
Coverage is a signal, not a goal. 100% line coverage with no branch assertions is worse than 80% coverage that exercises every meaningful decision point.
What actually matters:
What to exclude from coverage targets:
index.ts, main.py).Run the test suite using the command you identified in Step 0. Every item must be true:
.skip, .only (left accidentally), or xtest / xit in the file.beforeEach using the correct framework method.development
Generate, audit, or improve a project README following a 15-section structure (Title, Table of Contents, About, Features, Tech Stack, Architecture, Project Structure, Getting Started, Configuration, Security, How to Contribute, What's Next, License, Acknowledgements, Author) with Mermaid diagrams for architecture and flows. Use this skill whenever the user asks to "write a README", "create a README", "draft a README", "generate README.md", "scaffold project docs", "document this repo", "improve my README", "audit my README", "what should go in my README", or starts a new repository and needs documentation. Also trigger on phrases like "the README is bare", "this project has no docs", "fill in my README sections", or any request that produces or reviews a top-level repository README. The skill defaults to Mermaid for diagrams because it renders natively on GitHub, GitLab, Bitbucket, and most modern Markdown viewers — no external image hosting required.
tools
Use this skill whenever you need to commit changes or generate a conventional commit message for user review.
tools
Generate or update an ESLint plugin that exports rule configs compatible with ESLint v8 (eslintrc) and ESLint v9 (flat config).
documentation
Rewrites changelog entries with cheeky, narrative flair following project conventions. Use this when asked to rewrite or update CHANGELOG.md entries.