doing-a-simple-two-sta…
Use when analyzing a large corpus of text, code, or data that exceeds a single agent's effective context - orchestrates parallel Worker subagents, Critic…
Use when writing or reviewing tests - covers test philosophy, condition-based waiting, mocking strategy, and test isolation
$ npx -y skills add ed3dai/ed3d-plugins --skill writing-good-tests --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/writing-good-testsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing or reviewing tests - covers test philosophy, condition-based waiting, mocking strategy, and test isolation
name: writing-good-tests description: Use when writing or reviewing tests - covers test philosophy, condition-based waiting, mocking strategy, and test isolation user-invocable: false
**"Write tests. Not too many. Mostly integration."** — Kent C. Dodds
Tests verify real behavior, not implementation details. The goal is confidence that your code works, not coverage numbers.
**Core principles:** 1. Test behavior, not implementation — refactoring shouldn't break tests 2. Integration tests provide better confidence-to-cost ratio than unit tests 3. Wait for actual conditions, not arbitrary timeouts 4. Mock strategically — real dependencies when feasible, mocks for external systems 5. Don't pollute production code with test-only methods
Use **Arrange-Act-Assert** (or Given-When-Then):
test('user can cancel reservation', async () => {
// Arrange
const reservation = await createReservation({ userId: 'user-1', roomId: 'room-1' });
// Act
const result = await cancelReservation(reservation.id);
// Assert
expect(result.status).toBe('cancelled');
expect(await getReservation(reservation.id)).toBeNull();
});**One action per test.** Multiple assertions are fine if they verify the same behavior.
Flaky tests often guess at timing. This creates race conditions where tests pass locally but fail in CI.
**Wait for conditions, not time:**
// BAD: Guessing at timing await new Promise(r => setTimeout(r, 50)); const result = getResult(); // GOOD: Waiting for condition await waitFor(() => getResult() !== undefined); const result = getResult();
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}| Scenario | Pattern | |----------|---------| | Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | | Wait for state | `waitFor(() => machine.state === 'ready')` | | Wait for count | `waitFor(() => items.length >= 5)` |
Only when testing actual timing behavior (debounce, throttle, intervals):
// Testing tool that ticks every 100ms await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition await new Promise(r => setTimeout(r, 200)); // Then: wait for 2 ticks // Comment explains WHY: 200ms = 2 ticks at 100ms intervals
> "You don't hate mocks; you hate side-effects." — J.B. Rainsberger
Mocks reveal where side-effects complicate your code. Use them strategically, not reflexively.
Create thin wrappers around third-party libraries. Mock YOUR wrapper, not the library.
// BAD: Mock the HTTP client directly
const mockClient = vi.mocked(httpx.Client);
// GOOD: Create your own wrapper
class RegistryClient {
constructor(private client: HttpClient) {}
async getRepos() {
return this.client.get('https://registry.example.com/v2/_catalog');
}
}
// Mock your wrapper
vi.mock('./registry-client');This simplifies tests AND improves your design.
| Dependency Type | Example | Strategy | |-----------------|---------|----------| | **Managed** (you control it) | Your database, your file system | Use REAL instances | | **Unmanaged** (external) | Third-party APIs, SMTP, message bus | Use MOCKS |
Communications with managed dependencies are implementation details — you can refactor them freely. Communications with unmanaged dependencies are observable behavior — mocking protects against external changes.
// BAD: Testing that the mock exists
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
// GOOD: Test real behavior
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByRole('navigation')).toBeInTheDocument();
});**Gate:** Before asserting on any mock element, ask: "Am I testing real behavior or mock existence?"
// BAD: Mock breaks test logic
test('detects duplicate server', () => {
// Mock prevents config write that test depends on!
vi.mock('ToolCatalog', () => ({
discoverAndCacheTools: vi.fn().mockResolvedValue(undefined)
}));
await addServer(config);
await addServer(config); // Should throw - but won't!
});
// GOOD: Mock at correct level
test('detects duplicate server', () => {
vi.mock('MCPServerManager'); // Just mock slow server startup
await addServer(config); // Config written
await addServer(config); // Duplicate detected
});**Gate:** Before mocking, ask: "What side effects does this have? Does my test depend on them?"
Mock the COMPLETE data structure as it exists in reality:
// BAD: Partial mock
const mockResponse = {
status: 'success',
data: { userId: '123' }
// Missing: metadata that downstream code uses
};
// GOOD: Mirror real API
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' },
metadata: { requestId: 'req-789', timestamp: 1234567890 }
};Warning signs:
> "As the number of mocks grows, the probability of testing the mock instead of the desired code goes up." — Codurance
Consider integration tests with real componen
Ed's repo of Claude Code plugins, centered around a research-plan-implement workflow. Only a tiny bit cursed. If you're lucky.
Repo: ed3dai/ed3d-plugins
Use when analyzing a large corpus of text, code, or data that exceeds a single agent's effective context - orchestrates parallel Worker subagents, Critic…
Use when creating a new Claude Code plugin or setting up plugin structure - provides complete file organization, manifest format, and component definitions for…
Use when creating specialized subagents for Claude Code plugins or the Task tool - covers description writing for auto-delegation, tool selection, prompt…
Use when creating, releasing, or maintaining a Claude Code Plugin Marketplace - covers marketplace.json schema, version management, release checklists,…
Use when completing development phases or branches to identify and update CLAUDE.md or AGENTS.md files that may have become stale - analyzes what changed,…