一键导入
essential-test-design
Write tests that verify observable behavior (contract), not implementation details. Auto-invoked when writing or reviewing tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Write tests that verify observable behavior (contract), not implementation details. Auto-invoked when writing or reviewing tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | essential-test-design |
| description | Write tests that verify observable behavior (contract), not implementation details. Auto-invoked when writing or reviewing tests. |
Tests that are tightly coupled to implementation details cause two failures:
setTimeout was called)setTimeout to a delay() utility, spy breaks)Both undermine the purpose of testing: detecting regressions in behavior.
A test is "essential" when it:
Ask: "What does the caller of this function experience?" — test that.
// BAD: Tests implementation, not behavior
// Breaks if implementation changes from setTimeout to any other delay mechanism
const spy = vi.spyOn(global, 'setTimeout');
await exponentialBackoff(1);
expect(spy).toHaveBeenCalledWith(expect.any(Function), 1000);
// BAD: The "arrange" is set up only to make the "assert" trivially pass
// This is a self-fulfilling prophecy, not a meaningful test
vi.advanceTimersByTime(1000);
await promise;
// No assertion — "it didn't throw" is not a valuable test
// GOOD: Tests the observable contract
// "Does not resolve before the expected delay, resolves at the expected delay"
let resolved = false;
mailService.exponentialBackoff(1).then(() => { resolved = true });
await vi.advanceTimersByTimeAsync(999);
expect(resolved).toBe(false); // Catches: delay too short
await vi.advanceTimersByTimeAsync(1);
expect(resolved).toBe(true); // Catches: delay too long or hangs
When writing a test, ask these questions in order:
setTimeout to Bun.sleep() shouldn't break the testexpect(resolved).toBe(false) at 999ms would catch itUse fake timers + boundary assertions (as shown above).
Assert on output shape/values, not on which internal helper was called.
// BAD
const spy = vi.spyOn(utils, 'formatDate');
transform(input);
expect(spy).toHaveBeenCalled();
// GOOD
const result = transform(input);
expect(result.date).toBe('2026-01-01');
Mocking the boundary (API/DB) is acceptable — that IS the observable behavior.
// OK: The contract IS "sends an email via mailer"
expect(mockMailer.sendMail).toHaveBeenCalledWith(
expect.objectContaining({ to: 'user@example.com' })
);
Test the number of attempts and the final outcome, not the internal flow.
// GOOD: Contract = "retries N times, then fails with specific error"
mockMailer.sendMail.mockRejectedValue(new Error('fail'));
await expect(sendWithRetry(config, 3)).rejects.toThrow('failed after 3 attempts');
expect(mockMailer.sendMail).toHaveBeenCalledTimes(3);
GROWI main application (apps/app) specific patterns for Next.js, Jotai, SWR, and testing. Auto-invoked when working in apps/app.
GROWI testing patterns with Vitest, React Testing Library, and vitest-mock-extended (type-safe mocking, avoid type assertions). Auto-invoked when writing or reviewing tests.
Investigate a GitHub issue - fetch info, update labels, analyze code/reproduce, report findings, and optionally fix. Usage: /investigate-issue <issue-url-or-number>
GROWI main application (apps/app) specific commands and scripts. Auto-invoked when working in apps/app.
Auto-invoked when modifying page transition logic, global atom hydration, or the `[[...path]]` dynamic route. Explains the data flow from SSR/client navigation to page rendering, and the hydration-vs-subsequent-sync rule for global atoms (`currentPathnameAtom`, `currentUserAtom`, `isMaintenanceModeAtom`).
Organize and clean up specification documents after implementation completion. Removes implementation details while preserving essential context for future refactoring.