writing-tests
Skill4 starsChanged 7 days agoMITView on GitHub
Use when adding or reviewing a `*.test.ts` in this repo — decides whether code deserves a test at all, how many cases it earns, where the test file sits and which existing seam it mounts on. Applies whenever you are tempted to test a constant, a type, a barrel, a class-name map or a component's rendering, or need the project's store, component or composable harnesses.
39 other projects use a version of this.
Adopted by
Most recently taken up by
- mcp-use/mcp-usetheir own version, first seen Nov 8, 2026
- yuri-xyz/skillstheir own version, first seen Nov 8, 2026
- lox/agent-skillstheir own version, first seen Nov 7, 2026
- JetBrains/intellij-communitytheir own version, first seen Nov 7, 2026
- iliaal/ai-skillstheir own version, first seen Nov 7, 2026
- PostHog/posthog-fosstheir own version, first seen Nov 6, 2026
Used with
Projects with it also use reviewing-code, commits and 5 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
bostonaholicSkillreviewing-code
Code review methodology for evaluating implementation changes. Use when reviewing code changes for quality, design, correctness, and maintainability. Focuses on changes made during implementation using Conventional Comments for clear, actionable feedback.
Also in 4 projects that use this
JetBrainsSkillcommits
Use this skill whenever the user asks to commit changes, write or fix a commit message, amend or rename a commit, or do a workflow that includes committing in the IntelliJ repository. This is a thin repo-specific overlay: use IntelliJ commit format, write full commit messages by default, and keep requested suffixes such as IJ-MR trailers in a final separate paragraph.
Also in 3 projects that use this
OpenAISkillrunning-tests
Guide for running tests in the openai-dotnet repository. Use this when asked to run, debug, or validate tests, or when writing new tests. Explains test modes (Playback, Record, Live), how to identify recorded vs non-recorded tests, environment variable configuration, and what to do when recordings are missing or stale.
Also in 3 projects that use this
BlockSkilldebugging
Debugging guidelines and best practices
Also in 5 projects that use this
VercelSkilltechnical-writing
Write, edit, review, or audit user-facing documentation for the eve repository. Use for changes under docs/, documentation tied to eve APIs or CLI behavior, docs work based on Slack or support feedback, and requests to make eve docs clearer, more natural, or less AI-patterned while verifying claims against current source, tests, CLI help, public releases, and repository conventions.
Also in 3 projects that use this
SentrySkillsecurity-review
Finds exploitable application security vulnerabilities in code changes. Use for Warden security scans, appsec review, OWASP-style checks, authentication or authorization bugs, injection, XSS, SSRF, path traversal, secrets, unsafe crypto, webhook verification, open redirects, or sensitive data exposure.
Also in 3 projects that use this
Other versions39 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 39
- remotion-dev/remotion★ 72ksame name
- PostHog/posthog★ 40ksame name
- JetBrains/intellij-community★ 21ksame name
- quarkusio/quarkus★ 16ksame name
- mcp-use/mcp-use★ 11ksame name
- DetachHead/rebased★ 6.5ksame name
- chattocorp/chatto★ 2.9ksame name
- starkware-libs/cairo★ 1.9ksame name
- bholmesdev/hubble.md★ 1.5ksame name
- amplifthq/openmeld★ 1.4ksame name
29 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository12 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- keeping-symbols-localSkill
- naming-filesSkill
- release-dailySkill
- structuring-componentsSkill
- structuring-script-setupSkill
- structuring-utility-filesSkill
- writing-commentsSkill
- writing-composablesSkill
- writing-functions-and-classesSkill
- writing-typesSkill
The file
We split this skill into the rules it gives an agent
2 rules. 0 are common, 1 is distinctive, and 1 is very specific to this project.
0
Common
Most of the 39 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 71
When a file makes no decision and is not on this list, it still gets its two cases.
- Very specific: line 90
Run one file from the repo root: pnpm evitest run apps/desktop/tests/renderer/stores/tasks.store.test.ts.
“Common” is measured: kept by at least 6 in 10 of the 39 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing Tests
An honest test goes red before the fix, derives its expectation by hand, asserts on real behaviour rather than the mock, and detects a relevant mutation of the production code before it is kept. If dev-skills:tdd is available in the current agent, read it for the full workflow; these requirements apply when it is not.
This skill governs whether a test is worth writing, where it sits, and where it mounts.
Core rule
A test earns its place when it pins a decision — something a future change could alter silently, without a type error and without a crash.
A decision is one of:
- a rounding direction (
floorvsceil) or the step it rounds to - a threshold and its strictness (
<=vs<) - a mapping of one code to another, and its fallback
- precedence between two sources of the same fact (a remote change against a local one under Last-Write-Wins)
- ordering, grouping, deduplication
- absence expressed as
nullrather than a confident0 - a state transition — accepted / ignored as stale / removed
- a lifecycle guarantee — one shared in-flight promise, retry exactly once, a refcounted unsubscribe, subscribe before backfill, teardown on unmount
- what actually crosses
window.BridgeIPCor reaches the wire
Everything else is plumbing, and plumbing is covered by the decisions that run through it.
Budget
| The code under test | Cases it earns |
| Makes no decision | 2 — it works, and it fails the one way it can |
| Makes n decisions | n, one each |
Has a catch-all that turns any malformed input into null | 1 — the catch-all is one decision, not one per kind of malformed input |
| Branches over an enum where every arm is the same shape | 1 — the arm that is not a literal |
| Branches over an enum where the arms are different logic | one per arm — three formulas are three cases |
The suite is read by someone returning after a year to change something. Five hundred tests that each pin a decision are a safety net; five thousand that restate what the code obviously does are noise that teaches them to distrust the run.
Point it at the public surface
A private helper — anything not exported, or not on the public surface of a class, store or composable — gets no test of its own. It is covered through the caller that uses it.
Never widen visibility for a test. Exporting a helper, or turning a private method public, so a test can reach it is the change that makes the test worthless: it pins the internal shape that the next refactor was going to move, and stops pinning the behaviour someone actually depends on. A store or composable member that only tests read is removed from the public surface, and its test moves to the seam the app actually uses — the action the UI calls, or the util the store delegates to.
The payoff is exactly that refactor. When a private method breaks, the public one that uses it goes red — the same signal, at the level that still means something after the internals are rewritten.
a store's private helper → drive the public action, assert the state it leaves
a class's private method → drive the public method, assert the result and the emitted event
a composable's inner function → call what it returned, assert the value or the effectAnd assert the result, not the echo. "It took the parameters and handed them back" is not a behaviour — what a caller depends on is the value, the state or the effect that came out the other side. A test that only proves the arguments survived the trip passes on any implementation, including a broken one.
What gets no test
| Why | |
constants.ts, constants/**, enums, string tables | Nothing can change silently — a test would restate the value it reads |
types.ts, types/** | No runtime |
index.ts barrels, variants.ts, a <part>Class computed, a get<Part>Classes function or anything else that only assembles class names | Re-exports and class-name maps |
Delegation to a dependency — cn → twMerge(clsx()) | Tests the library, not us |
A thin request wrapper — one call to the HTTP client or window.BridgeIPC, nothing decided around it | Its test exercises a mock, never the service |
| One-line key builders — a function that only fills a template string | The template string is already the assertion |
| Code nothing reaches today, kept by a recorded decision | No test there protects anything; intricacy is not reach |
| A component's rendering, CSS classes, animations, transitions; that it mounts | Appearance is checked in the running app; that it mounts is the framework's job |
When a file makes no decision and is not on this list, it still gets its two cases. When it is on this list, it gets none — say so and move on.
Components and composables
Logic is tested through the composable that holds it. A component's logic lives in a composable (structuring-components, writing-composables) and is tested there; the component keeps no test of its own for it. A component gets a unit test only for logic it still holds itself — that is rare, and when the logic moves to a composable, the test moves with it.
The guarantees of a shared composable are tested once, at the composable. Its consumers do not repeat them and test only their own decisions.
A component itself is looked at in the running app, over CDP. A test that only proves a component mounts or renders is never written: it tests the framework.
Where tests live
Tests never sit beside the source. They mirror it under a tests/ root:
apps/desktop/tests/renderer/…— the renderer (stores/,modules/,components/,composables/,overlays/,views/,utils/)apps/desktop/tests/main/…,apps/desktop/tests/shared/…— the main process and shared codeapps/server/tests/…— the sync serverpackages/<pkg>/tests/…—core,protocol,std,tools
Run one file from the repo root: pnpm evitest run apps/desktop/tests/renderer/stores/tasks.store.test.ts. The suite runs under Electron's Node (evitest) — plain vitest cannot load better-sqlite3.
Seams
Mount on one that already exists. The ideal number of new seams is zero: a test that needs production code restructured before it can be written is reporting a design problem — that is a refactor, not a test, and it is resolved before the test is written.
| Testing | Harness | Exemplar |
| A pure function | none — call it | apps/desktop/tests/renderer/components/MarkdownEditor/toImageAltText.test.ts |
| A store | mockBridgeIPC() + setActivePinia(createPinia()) + vi.mock of the renderer api; import the store after the mocks | apps/desktop/tests/renderer/stores/branches.store.test.ts |
| A component's own logic, the rare case | mount from @vue/test-utils + trigger, assert on the DOM or on emitted() | apps/desktop/tests/renderer/components/MessageReasoning.test.ts |
| A module over real stores | mockBridgeIPC() + a real Pinia + mount | apps/desktop/tests/renderer/modules/CalendarDock/CalendarDock.test.ts |
| A composable with lifecycle hooks | mount(defineComponent({setup() { …; return () => h("div") }})), so onUnmounted attaches | mountBoardDrag in apps/desktop/tests/helpers/boardDrag.ts |
| Layout: scroll position, overflow, sizes | none — happy-dom lays nothing out; check it in the running app over CDP (dev-skills:verify) | — |
| Timers, streaming, retry, debounce, the clock | vi.useFakeTimers ({toFake: ["Date"]} when only the clock matters) + advanceTimersByTimeAsync, awaiting a rejection before advancing | MessageReasoning.test.ts, QuickTask.test.ts |
| The Markdown editor | Mount the editor, then use findTipTapEditor, typeTipTapText and pressTipTapKey from apps/desktop/tests/helpers/tiptapEditor.ts | apps/desktop/tests/renderer/components/MarkdownEditor/MarkdownEditor.test.ts |
| The AI agent loop | apps/desktop/tests/main/ai/helpers/agentFixture.ts, mockAiClient.ts | apps/desktop/tests/main/ai/AIController.test.ts |
| Storage core over SQLite | packages/core/tests/helpers/db.ts | packages/core/tests/storage/… |
A unit assertion on a layout proxy (a height, a class) says it is a proxy in the test's name — the (layout proxy) cases in apps/desktop/tests/renderer/modules/TaskBoard/TaskBoard.test.ts.
Shared renderer harnesses live in apps/desktop/tests/helpers/, one harness per file — mockBridgeIPC is the one way to stand in for window.BridgeIPC. When a second test file needs the same harness, it moves there and is imported from there — never copied into a test file.
Conventions
- No comments, same as production code. A case that needs explaining needs a better name.
- Name the test as a sentence about behaviour, so the diagnosis reads out of the failure:
"createBranch trims the name before it reaches the API", not"works". - Build fixtures through a
make*factory with overrides (makeMilestone({targetDate: …})), never a hand-copied object per case. - Mock at the module level with
vi.mock, and build the mock object withvi.hoistedwhen the factory needs it. MSW is not a dependency and is not added for it.
Common mistakes
- Covering a file because it is untested rather than because it decides something. Coverage is the by-product; the decision is the reason.
- Spending five cases on a guard clause that has one behaviour, then giving a three-formula function one case because "the switch is covered".
- Asserting
toHaveBeenCalledTimeson a mock as the whole test. That passes because the mock exists — assert on the state or the value the code produced. - Testing that a component mounts, or what it renders. Look at it in the running app; test its logic at the composable.
- Writing a test against code nothing reaches because its logic looks intricate.
- Hand-rolling a
window.BridgeIPCstub instead ofmockBridgeIPC(). - Copying a harness into a test file instead of importing it.
- Deciding a file is untested because no test mirrors its path. Test files here are sometimes grouped —
TagsDock.test.tsalso drivesuseTaskColumns. Work out what is covered from what the test files import, never from filenames, or you will plan work that already exists.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

