
testing-anti-patterns
by bacchus-labs
Claude Code plugin for keeping the ponies on their leads.
SKILL.md
name: testing-anti-patterns description: 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
Testing Anti-Patterns
Skill Usage Announcement
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.
Overview
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.
The Iron Laws
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
Anti-Pattern 1: Testing Mock Behavior
The violation:
// ❌ BAD: Testing that the mock exists
test('renders sidebar', () => {
render(<Page />);
expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
});
Why this is wrong:
- You're verifying the mock works, not that the component works
- Test passes when mock is present, fails when it's not
- Tells you nothing about real behavior
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
Gate Function
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
Anti-Pattern 2: Test-Only Methods in Production
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:
- Production class polluted with test-only code
- Dangerous if accidentally called in production
- Violates YAGNI and separation of concerns
- Confuses object lifecycle with entity lifecycle
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));
Gate Function
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
Anti-Pattern 3: Mocking Without Understanding
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:
- Mocked method had side effect test depended on (writing config)
- Over-mocking to "be safe" breaks actual behavior
- Test passes for wrong reason or fails mysteriously
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 ✓
});
Gate Function
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
Anti-Pattern 4: Incomplete Mocks
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:
- Partial mocks hide structural assumptions - You only mocked fields you know about
- Downstream code may depend on fields you didn't include - Silent failures
- Tests pass but integration fails - Mock incomplete, real API complete
- False confidence - Test proves nothing about real behavior
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
};
Gate Function
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
Anti-Pattern 5: Integration Tests as Afterthought
The violation:
✅ Implementation complete
❌ No tests written
"Ready for testing"
Why this is wrong:
- Testing is part of implementation, not optional follow-up
- TDD would have caught this
- Can't claim complete without tests
The fix:
TDD cycle:
1. Write failing test
2. Implement to pass
3. Refactor
4. THEN claim complete
Anti-Pattern 6: Testing Implementation Details (Frontend)
The violation: Testing internal component state, props, or hooks instead of user-visible behavior.
Why This Is Wrong
Users don't see internal state:
- React state, Vue data, Angular component properties
- Tests break when refactoring (state → context → store)
- Doesn't verify UI actually updates
Tests become brittle:
- Change from state to props → test breaks
- Extract to hook → test breaks
- Move to external store → test breaks
Misses real bugs:
- State updates but UI doesn't render
- UI renders but wrong value displayed
- Accessibility broken (no ARIA labels)
Examples
BAD: Testing Internal State
// ❌ BAD: Testing React state directly
test('counter increments', () => {
const { result } = renderHook(() => useCounter());
result.current.increment();
expect(result.current.count).toBe(1);
});
Why wrong:
- Users don't see
result.current.count - Tests implementation (state), not behavior (UI)
- Breaks when refactoring to props or context
GOOD: Testing User-Visible Behavior
// ✅ 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:
- Tests what users experience
- Survives refactoring (state → props → context)
- Catches UI rendering bugs
More Examples
BAD: Testing Props
// ❌ 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 Behavior
// ✅ 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
// ❌ BAD: Testing CSS classes
test('button is red', () => {
render(<Button variant="danger" />);
expect(screen.getByRole('button')).toHaveClass('btn-danger');
});
GOOD: Testing Visual Appearance
// ✅ GOOD: Visual regression test
test('button is red', async ({ page }) => {
await mount('<custom-button variant="danger"></custom-button>');
await expect(page.locator('button')).toHaveScreenshot();
});
Gate Function
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)
Query Priority (Testing Library)
// 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');
Framework-Agnostic Pattern
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();
});
Anti-Pattern 7: No Accessibility Testing
The violation: Shipping UI without verifying it's accessible to users with disabilities.
Why This Is Wrong
Legal requirement:
- WCAG 2.1 Level AA compliance mandatory in many industries
- ADA lawsuits for inaccessible websites
- Section 508 compliance for government contracts
Excludes users:
- ~2% of users rely on screen readers
- Many more use keyboard navigation
- Inaccessible UI is unusable for these users
Accessibility issues ARE bugs:
- Button with no accessible name → can't be used
- Form input with no label → can't be filled
- Modal without focus trap → keyboard users stuck
Examples
BAD: No Accessibility Testing
// ❌ 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:
- Might be
<div onclick>(not keyboard accessible) - Might have no ARIA label (screen readers can't announce)
- Might not have visible focus (keyboard users lost)
GOOD: Comprehensive Accessibility Testing
// ✅ 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();
});
Common Accessibility Violations
1. Buttons Without Accessible Names
<!-- ❌ 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>
2. Divs as Buttons
<!-- ❌ BAD: Not keyboard accessible -->
<div onclick="submit()">Submit</div>
<!-- ✅ GOOD: Semantic button -->
<button onclick="submit()">Submit</button>
3. Form Inputs Without Labels
<!-- ❌ BAD: No label for screen readers -->
<input type="text" placeholder="Email" />
<!-- ✅ GOOD: Explicit label -->
<label for="email">Email</label>
<input id="email" type="text" />
Gate Function
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
Framework-Agnostic Testing
// 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);
});
Anti-Pattern 8: Testing Happy Path Only
The violation: Only testing successful scenarios, ignoring loading, error, and empty states.
Why This Is Wrong
Real users experience all states:
- Loading (while fetching data)
- Error (network failure, 500 error, timeout)
- Empty (no data available)
- Partial (some data missing)
Production bugs occur in error paths:
- App crashes on network error
- Blank screen on empty data
- No loading indicator (users confused)
- Error messages missing or unclear
Incomplete mental model:
- Don't understand component behavior fully
- Surprised by production bugs
- No guidance for handling failures
Examples
BAD: Only Happy Path
// ❌ BAD: Only tests success case
test('loads user profile', async () => {
render(<UserProfile userId="123" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
Why wrong:
- What if API is slow? (no loading test)
- What if API fails? (no error test)
- What if user not found? (no empty test)
- Production users will experience these!
GOOD: All States Tested
// ✅ 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();
});
UI State Checklist
For each UI component, test:
- Loading state: Spinner/skeleton renders while fetching
- Success state: Data displays correctly
- Error state: Error message displays on failure
- Empty state: Empty message when no data
- Partial state: Handles missing fields gracefully
- Disabled state: Buttons disabled during actions
- Validation state: Error messages for invalid input
Gate Function
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
Framework-Agnostic Pattern
// 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)
});
Common State Combinations
// 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();
});
});
When Mocks Become Too Complex
Warning signs:
- Mock setup longer than test logic
- Mocking everything to make test pass
- Mocks missing methods real components have
- Test breaks when mock changes
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
TDD Prevents These Anti-Patterns
Why TDD helps:
- Write test first → Forces you to think about what you're actually testing
- Watch it fail → Confirms test tests real behavior, not mocks
- Minimal implementation → No test-only methods creep in
- Real dependencies → You see what the test actually needs before mocking
If you're testing mock behavior, you violated TDD - you added mocks without watching test fail against real code first.
Quick Reference
| 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 |
Red Flags
- Assertion checks for
*-mocktest IDs - Methods only called in test files
- Mock setup is >50% of test
- Test fails when you remove mock
- Can't explain why mock is needed
- Mocking "just to be safe"
Examples
Example: Complete Frontend Testing
// 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 () => { /* ... */ });
Integration with Other Skills
Related skills:
- test-driven-development: Write tests covering all states BEFORE implementing
- verification-before-completion: Verify accessibility and all states tested
- frontend-visual-regression-testing: Use for visual appearance testing
- frontend-accessibility-verification: Use for comprehensive a11y testing
- frontend-component-testing: Test components in isolation with all states
The Bottom Line
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.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon