Instructions
When the user asks for help with tests (writing new tests, improving existing ones, or defining testing standards), follow these rules:
-
Test intent, not implementation
- Make tests read like requirements.
- Assert observable behaviour: return values, DOM, side-effects, and public contracts.
- Avoid asserting private state, internal variables, or exact call sequences unless the behaviour requires it.
-
Favour high-signal, low-maintenance tests
- A test should fail only when the behavioural contract changes.
- Use the smallest, clearest input that still demonstrates the behaviour.
- Name tests in real-world language, e.g.:
it("allows a customer to update their email", async () => {})
-
Avoid flakiness with strict control of nondeterminism
- Control time (fake timers), randomness, and external systems.
- Do not hit real network, databases, or filesystem in unit/integration tests.
- Explicitly await all async behaviour (no untracked promises).
-
Mock boundaries, not internals
- Mock HTTP via tools like Mock Service Worker (MSW) or equivalent.
- Do not mock your own internal functions when you can mock the external boundary instead.
- When mocking, model both success and failure modes (errors, invalid data, timeouts).
-
Prefer integration tests where possible
- Use unit tests for pure functions and complex branching.
- Use integration tests for behaviour spanning multiple layers.
- Keep end-to-end tests few and focused on critical user flows.
-
Refactor for testability
- If a test is hard to write, consider changing the production code:
- Extract pure functions.
- Inject dependencies instead of using globals.
- Isolate side-effects behind small, well-defined boundaries.
- If a test is hard to write, consider changing the production code:
-
Use the
usingkeyword for resource cleanup- Use
usingfor disposable resources such as:- Temporary files/directories
- DB connections or transactions
- Event listeners
- Fake timers or custom resources exposing
Symbol.dispose
- Prefer
usingover manualafterEachcleanup when possible.
Example:
it("handles a temporary directory", async () => { using tmp = createTempDir(); await writeFile(tmp.path + "/a.txt", "hello"); expect(await readFile(tmp.path + "/a.txt")).toBe("hello"); }); - Use
-
Recommended test layout
- When generating or refactoring tests, aim for this structure:
describe("FeatureName", () => { it("states the requirement", async () => { // Arrange const user = buildUser(); // Act const result = await updateEmail(user.id, "new@x.com"); // Assert expect(result.email).toBe("new@x.com"); }); });
- When generating or refactoring tests, aim for this structure:
-
Golden rules summary
- Test the intention, not the implementation.
- Mock boundaries, not internal helpers.
- Keep tests deterministic and repeatable.
- Prefer integration tests for real behaviour.
- Refactor code that is hard to test.
- Use MSW or equivalent for HTTP boundaries.
- Use
usingfor automatic cleanup of disposable resources. - Keep inputs minimal and test names descriptive.
When responding, ensure that any example tests you write or refactor obey these rules by default. If the user’s existing tests violate these rules, explain what to change and why.
Examples
Example 1: Generating tests
User prompt
I have a function
updateEmail(userId, email)that calls an HTTP API. Can you write tests for it?
What to do
- Propose integration-style tests that:
- Call
updateEmailas the user would. - Mock HTTP via MSW or a boundary-level mock.
- Assert on the returned result and/or observable side-effects.
- Call
- Avoid spying on internal helpers like
apiClient.get.
Example response snippet
describe("updateEmail", () => {
it("updates the user email when the API succeeds", async () => {
// MSW handler: mock boundary, not internal client
server.use(
http.put("/users/:id/email", () => HttpResponse.json({ email: "new@example.com" }))
);
const result = await updateEmail("user-123", "new@example.com");
expect(result.email).toBe("new@example.com");
});
it("throws a helpful error when the API fails", async () => {
server.use(
http.put("/users/:id/email", () => HttpResponse.text("Server error", { status: 500 }))
);
await expect(updateEmail("user-123", "new@example.com"))
.rejects
.toThrow(/unable to update email/i);
});
});
Example 2: Refactoring brittle tests
User prompt
My test keeps breaking whenever I refactor. It spies on
apiClient.putand checks the exact arguments. How can I make it better?
What to do
- Explain that the test is implementation-coupled.
- Suggest asserting on behaviour instead of specific internal calls.
- Optionally replace internal spies with boundary mocks (e.g. MSW).
Example response snippet
// Instead of:
expect(apiClient.put).toHaveBeenCalledWith("/users/123/email", { email: "new@example.com" });
// Prefer:
const result = await updateEmail("123", "new@example.com");
expect(result.email).toBe("new@example.com");
Explain that this test will only fail when the behavioural contract changes, not just when apiClient usage is refactored.
Related Skills
/tdd-integration- TDD Red-Green-Refactor cycle for feature development/mw-mr-mf- Make it Work/Right/Fast workflow for iterative development