Use when writing or changing tests, adding mocks, or tempted to add test-only methods to production code - prevents testing mock behavior, production pollution with test-only methods, and mocking without understanding dependencies
Detects testing anti-patterns when writing or modifying tests. Triggers when you're creating mocks, adding test-only methods to production code, or testing UI without verifying accessibility and all user states.
/plugin marketplace add samjhecht/wrangler/plugin install wrangler@samjhecht-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
MANDATORY: When using this skill, announce it at the start with:
š§ Using Skill: testing-anti-patterns | [brief purpose based on context]
Example:
š§ Using Skill: testing-anti-patterns | [Provide context-specific example of what you're doing]
This creates an audit trail showing which skills were applied during the session.
Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
Core principle: Test what the code does, not what the mocks do.
Following strict TDD prevents these anti-patterns.
1. NEVER test mock behavior
2. NEVER add test-only methods to production classes
3. NEVER mock without understanding dependencies
4. NEVER test implementation details - test user-visible behavior
5. NEVER ship UI without accessibility testing
6. NEVER test only happy path - test all UI states
The violation:
// ā BAD: Testing that the mock exists
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
Why this is wrong:
your human partner's correction: "Are we testing the behavior of a mock?"
The fix:
// ā
GOOD: Test real component or don't mock it
test('renders sidebar', () => {
render(<Page />); // Don't mock sidebar
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// OR if sidebar must be mocked for isolation:
// Don't assert on the mock - test Page's behavior with sidebar present
BEFORE asserting on any mock element:
Ask: "Am I testing real component behavior or just mock existence?"
IF testing mock existence:
STOP - Delete the assertion or unmock the component
Test real behavior instead
The violation:
// ā BAD: destroy() only used in tests
class Session {
async destroy() { // Looks like production API!
await this._workspaceManager?.destroyWorkspace(this.id);
// ... cleanup
}
}
// In tests
afterEach(() => session.destroy());
Why this is wrong:
The fix:
// ā
GOOD: Test utilities handle test cleanup
// Session has no destroy() - it's stateless in production
// In test-utils/
export async function cleanupSession(session: Session) {
const workspace = session.getWorkspaceInfo();
if (workspace) {
await workspaceManager.destroyWorkspace(workspace.id);
}
}
// In tests
afterEach(() => cleanupSession(session));
BEFORE adding any method to production class:
Ask: "Is this only used by tests?"
IF yes:
STOP - Don't add it
Put it in test utilities instead
Ask: "Does this class own this resource's lifecycle?"
IF no:
STOP - Wrong class for this method
The violation:
// ā 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!
});
Why this is wrong:
The fix:
// ā
GOOD: Mock at correct level
test('detects duplicate server', () => {
// Mock the slow part, preserve behavior test needs
vi.mock('MCPServerManager'); // Just mock slow server startup
await addServer(config); // Config written
await addServer(config); // Duplicate detected ā
});
BEFORE mocking any method:
STOP - Don't mock yet
1. Ask: "What side effects does the real method have?"
2. Ask: "Does this test depend on any of those side effects?"
3. Ask: "Do I fully understand what this test needs?"
IF depends on side effects:
Mock at lower level (the actual slow/external operation)
OR use test doubles that preserve necessary behavior
NOT the high-level method the test depends on
IF unsure what test depends on:
Run test with real implementation FIRST
Observe what actually needs to happen
THEN add minimal mocking at the right level
Red flags:
- "I'll mock this to be safe"
- "This might be slow, better mock it"
- Mocking without understanding the dependency chain
The violation:
// ā BAD: Partial mock - only fields you think you need
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' }
// Missing: metadata that downstream code uses
};
// Later: breaks when code accesses response.metadata.requestId
Why this is wrong:
The Iron Rule: Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
The fix:
// ā
GOOD: Mirror real API completeness
const mockResponse = {
status: 'success',
data: { userId: '123', name: 'Alice' },
metadata: { requestId: 'req-789', timestamp: 1234567890 }
// All fields real API returns
};
BEFORE creating mock responses:
Check: "What fields does the real API response contain?"
Actions:
1. Examine actual API response from docs/examples
2. Include ALL fields system might consume downstream
3. Verify mock matches real response schema completely
Critical:
If you're creating a mock, you must understand the ENTIRE structure
Partial mocks fail silently when code depends on omitted fields
If uncertain: Include all documented fields
The violation:
ā
Implementation complete
ā No tests written
"Ready for testing"
Why this is wrong:
The fix:
TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete
The violation: Testing internal component state, props, or hooks instead of user-visible behavior.
Users don't see internal state:
Tests become brittle:
Misses real bugs:
// ā BAD: Testing React state directly
test('counter increments', () => {
const { result } = renderHook(() => useCounter());
result.current.increment();
expect(result.current.count).toBe(1);
});
Why wrong:
result.current.count// ā
GOOD: Testing what user sees
test('counter increments when button clicked', async () => {
render(<Counter />);
// User sees initial count
expect(screen.getByText('Count: 0')).toBeInTheDocument();
// User clicks button
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
// User sees updated count
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
Why correct:
// ā BAD: Testing component props
test('button has onClick prop', () => {
const onClick = jest.fn();
const { container } = render(<Button onClick={onClick} />);
expect(container.firstChild.props.onClick).toBe(onClick);
});
// ā
GOOD: Testing click behavior
test('button calls onClick when clicked', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Submit</Button>);
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
expect(onClick).toHaveBeenCalledTimes(1);
});
// ā BAD: Testing CSS classes
test('button is red', () => {
render(<Button variant="danger" />);
expect(screen.getByRole('button')).toHaveClass('btn-danger');
});
// ā
GOOD: Visual regression test
test('button is red', async ({ page }) => {
await mount('<custom-button variant="danger"></custom-button>');
await expect(page.locator('button')).toHaveScreenshot();
});
BEFORE querying component internals:
Ask: "Can a user see this value on screen?"
IF no (state, props, hooks, refs, CSS classes):
STOP - Don't test it directly
Test the visible outcome instead
IF yes (rendered text, ARIA labels, enabled/disabled state):
Test via user-facing queries:
- getByRole (best - accessible)
- getByLabelText (forms)
- getByText (visible text)
- NOT: getByTestId (last resort)
// 1. BEST: Accessible queries (what screen readers see)
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { name: /welcome/i });
screen.getByLabelText('Email address');
// 2. GOOD: Semantic queries (visible text)
screen.getByText('Click here to continue');
screen.getByPlaceholderText('Enter your email');
// 3. ACCEPTABLE: Test IDs (when nothing else works)
screen.getByTestId('submit-button');
// 4. NEVER: Implementation details
component.state.isSubmitting;
component.props.onClick;
wrapper.find('.submit-button');
Works with React, Vue, Angular, Svelte, Web Components:
// Test user behavior, not framework internals
test('form submits with valid data', async () => {
// Render component (framework-specific mount)
render(<LoginForm />);
// User interactions (framework-agnostic)
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
await userEvent.type(screen.getByLabelText('Password'), 'password123');
await userEvent.click(screen.getByRole('button', { name: /submit/i }));
// Verify user-visible outcome (framework-agnostic)
expect(await screen.findByText('Login successful')).toBeInTheDocument();
});
The violation: Shipping UI without verifying it's accessible to users with disabilities.
Legal requirement:
Excludes users:
Accessibility issues ARE bugs:
// ā BAD: Only tests visual rendering
test('button works', async ({ page }) => {
await page.click('.icon-button');
// No keyboard navigation test
// No screen reader verification
// No ARIA label check
});
Why wrong:
<div onclick> (not keyboard accessible)// ā
GOOD: Accessibility verification
test('button is accessible', async ({ page }) => {
await mount('<custom-button>Submit</custom-button>');
// Automated accessibility check
await injectAxe(page);
const violations = await checkA11y(page);
expect(violations).toHaveLength(0);
// Keyboard navigation
await page.keyboard.press('Tab');
await expect(page.locator('button')).toBeFocused();
await page.keyboard.press('Enter');
// Verify button activated
// Screen reader verification
const accessibleName = await page.locator('button').getAttribute('aria-label')
|| await page.locator('button').textContent();
expect(accessibleName).toBeTruthy();
});
<!-- ā BAD: Icon button with no label -->
<button><img src="close.svg" /></button>
<!-- ā
GOOD: Aria-label provided -->
<button aria-label="Close"><img src="close.svg" alt="" /></button>
<!-- ā BAD: Not keyboard accessible -->
<div onclick="submit()">Submit</div>
<!-- ā
GOOD: Semantic button -->
<button onclick="submit()">Submit</button>
<!-- ā BAD: No label for screen readers -->
<input type="text" placeholder="Email" />
<!-- ā
GOOD: Explicit label -->
<label for="email">Email</label>
<input id="email" type="text" />
BEFORE claiming UI work complete:
Has accessibility testing been done?
NO ā STOP - Run accessibility tests
axe-core violations: 0?
NO ā STOP - Fix violations before proceeding
Keyboard navigation tested?
NO ā STOP - Test Tab, Enter, Escape keys
All interactive elements have accessible names?
NO ā STOP - Add ARIA labels or text content
Lighthouse accessibility score ā„95?
NO ā STOP - Investigate and fix issues
ONLY THEN: Claim UI complete
// Works with Playwright, Selenium, Puppeteer
test('component is accessible', async ({ page }) => {
// Navigate to component
await page.goto('/checkout');
// Inject axe-core
await injectAxe(page);
// Run automated accessibility audit
const violations = await checkA11y(page);
expect(violations).toHaveLength(0);
});
The violation: Only testing successful scenarios, ignoring loading, error, and empty states.
Real users experience all states:
Production bugs occur in error paths:
Incomplete mental model:
// ā BAD: Only tests success case
test('loads user profile', async () => {
render(<UserProfile userId="123" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
Why wrong:
// ā
GOOD: Loading state
test('shows loading spinner initially', () => {
render(<UserProfile userId="123" />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
// ā
GOOD: Success state
test('shows user data when loaded', async () => {
mockAPI.getUser.mockResolvedValue({ name: 'Alice', email: 'alice@example.com' });
render(<UserProfile userId="123" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
// ā
GOOD: Error state
test('shows error message when fetch fails', async () => {
mockAPI.getUser.mockRejectedValue(new Error('Network error'));
render(<UserProfile userId="123" />);
expect(await screen.findByRole('alert')).toHaveTextContent('Failed to load profile');
});
// ā
GOOD: Empty state
test('shows empty message when user not found', async () => {
mockAPI.getUser.mockResolvedValue(null);
render(<UserProfile userId="123" />);
expect(await screen.findByText('User not found')).toBeInTheDocument();
});
// ā
GOOD: Partial state
test('handles missing email gracefully', async () => {
mockAPI.getUser.mockResolvedValue({ name: 'Alice', email: null });
render(<UserProfile userId="123" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
expect(screen.queryByText('Email:')).not.toBeInTheDocument();
});
For each UI component, test:
BEFORE writing component test:
List all possible UI states:
- Loading (fetching data)
- Success (data loaded)
- Error (network failure, 500, timeout)
- Empty (no data available)
- Partial (some fields missing)
- Disabled (buttons disabled during actions)
Write one test per state.
IF you only wrote one test:
STOP - You're testing incompletely
Add tests for other states
// Works with any framework (React, Vue, Angular, etc.)
test('handles all states', async () => {
// Initial: Loading
render(<Component />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
// After load: Success or Error or Empty
await waitFor(() => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
// Verify final state
// (success data, error message, or empty message)
});
// Form submission states
test('button disabled while submitting', async () => {
render(<CheckoutForm />);
const button = screen.getByRole('button', { name: /submit/i });
// Initial: Enabled
expect(button).toBeEnabled();
// During submit: Disabled
await userEvent.click(button);
expect(button).toBeDisabled();
expect(screen.getByText('Submitting...')).toBeInTheDocument();
// After submit: Enabled (if error) or Success message
await waitFor(() => {
expect(button).toBeEnabled();
});
});
Warning signs:
your human partner's question: "Do we need to be using a mock here?"
Consider: Integration tests with real components often simpler than complex mocks
Why TDD helps:
If you're testing mock behavior, you violated TDD - you added mocks without watching test fail against real code first.
| Anti-Pattern | Fix |
|---|---|
| Assert on mock elements | Test real component or unmock it |
| Test-only methods in production | Move to test utilities |
| Mock without understanding | Understand dependencies first, mock minimally |
| Incomplete mocks | Mirror real API completely |
| Tests as afterthought | TDD - tests first |
| Testing implementation details (frontend) | Test user-visible behavior with accessible queries |
| No accessibility testing | Run axe-core, test keyboard navigation, verify ARIA |
| Testing happy path only | Test all states: loading, error, empty, partial |
| Over-complex mocks | Consider integration tests |
*-mock test IDs// Anti-Pattern 6: Testing implementation details
// ā BAD
expect(component.state.isLoading).toBe(true);
// ā
GOOD
expect(screen.getByRole('progressbar')).toBeInTheDocument();
// Anti-Pattern 7: No accessibility testing
// ā BAD
await page.click('.close-button');
// ā
GOOD
await injectAxe(page);
await checkA11y(page);
await page.click('button[aria-label="Close"]');
// Anti-Pattern 8: Testing happy path only
// ā BAD
test('loads data', async () => {
expect(await screen.findByText('Data')).toBeInTheDocument();
});
// ā
GOOD
test('shows loading state', () => { /* ... */ });
test('shows data when loaded', async () => { /* ... */ });
test('shows error when fetch fails', async () => { /* ... */ });
test('shows empty state when no data', async () => { /* ... */ });
Related skills:
Mocks are tools to isolate, not things to test.
If TDD reveals you're testing mock behavior, you've gone wrong.
Fix: Test real behavior or question why you're mocking at all.
Master defensive Bash programming techniques for production-grade scripts. Use when writing robust shell scripts, CI/CD pipelines, or system utilities requiring fault tolerance and safety.