Detect test smells, overmocking, flaky tests, and coverage issues. Analyze test effectiveness, maintainability, and reliability. Use when reviewing tests or improving test quality.
/plugin marketplace add laurigates/claude-plugins/plugin install testing-plugin@lgates-claude-pluginsThis skill is limited to using the following tools:
Expert knowledge for analyzing and improving test quality - detecting test smells, overmocking, insufficient coverage, and other testing anti-patterns.
Test Quality Dimensions
Problem: Mocking too many dependencies, making tests fragile and disconnected from reality.
// ❌ BAD: Overmocked
test('calculate total', () => {
const mockAdd = vi.fn(() => 10)
const mockMultiply = vi.fn(() => 20)
const mockSubtract = vi.fn(() => 5)
// Testing implementation, not behavior
const result = calculate(mockAdd, mockMultiply, mockSubtract)
expect(result).toBe(15)
})
// ✅ GOOD: Mock only external dependencies
test('calculate order total', () => {
const mockPricingAPI = vi.fn(() => ({ tax: 0.1, shipping: 5 }))
const order = { items: [{ price: 10 }, { price: 20 }] }
const total = calculateTotal(order, mockPricingAPI)
expect(total).toBe(38) // 30 + 3 tax + 5 shipping
})
Detection:
Fix:
Problem: Tests break with unrelated code changes.
// ❌ BAD: Fragile selector
test('submits form', async ({ page }) => {
await page.locator('.form-container > div:nth-child(2) > button').click()
})
// ✅ GOOD: Semantic selector
test('submits form', async ({ page }) => {
await page.getByRole('button', { name: 'Submit' }).click()
})
# ❌ BAD: Tests implementation details
def test_user_creation():
user = User()
user._internal_id = 123 # Testing private attribute
assert user._internal_id == 123
# ✅ GOOD: Tests public interface
def test_user_creation():
user = User(id=123)
assert user.get_id() == 123
Detection:
Fix:
Problem: Tests pass or fail non-deterministically.
// ❌ BAD: Race condition
test('loads data', async () => {
fetchData()
await new Promise(resolve => setTimeout(resolve, 1000))
expect(data).toBeDefined()
})
// ✅ GOOD: Proper async handling
test('loads data', async () => {
const data = await fetchData()
expect(data).toBeDefined()
})
# ❌ BAD: Time-dependent test
def test_expires_in_one_hour():
token = create_token()
time.sleep(3601)
assert token.is_expired()
# ✅ GOOD: Inject time dependency
def test_expires_in_one_hour():
now = datetime(2024, 1, 1, 12, 0)
future = datetime(2024, 1, 1, 13, 1)
token = create_token(now)
assert token.is_expired(future)
Detection:
sleep() or setTimeout()Fix:
Problem: Similar test logic repeated across multiple tests.
// ❌ BAD: Duplicated setup
test('user can edit profile', async ({ page }) => {
await page.goto('/login')
await page.fill('[name=email]', 'user@example.com')
await page.fill('[name=password]', 'password')
await page.click('button[type=submit]')
await page.goto('/profile')
// Test logic...
})
test('user can view settings', async ({ page }) => {
await page.goto('/login')
await page.fill('[name=email]', 'user@example.com')
await page.fill('[name=password]', 'password')
await page.click('button[type=submit]')
await page.goto('/settings')
// Test logic...
})
// ✅ GOOD: Extract to fixture/helper
async function loginAsUser(page) {
await page.goto('/login')
await page.fill('[name=email]', 'user@example.com')
await page.fill('[name=password]', 'password')
await page.click('button[type=submit]')
}
test('user can edit profile', async ({ page }) => {
await loginAsUser(page)
await page.goto('/profile')
// Test logic...
})
Detection:
Fix:
beforeEach() hooksProblem: Tests take too long to run, slowing down feedback loop.
// ❌ BAD: Unnecessary setup in every test
describe('User API', () => {
beforeEach(async () => {
await database.migrate() // Slow!
await seedDatabase() // Slow!
})
test('creates user', async () => {
const user = await createUser({ name: 'John' })
expect(user.name).toBe('John')
})
test('updates user', async () => {
const user = await createUser({ name: 'John' })
await updateUser(user.id, { name: 'Jane' })
expect(user.name).toBe('Jane')
})
})
// ✅ GOOD: Shared expensive setup
describe('User API', () => {
beforeAll(async () => {
await database.migrate()
await seedDatabase()
})
beforeEach(async () => {
await cleanUserTable() // Fast!
})
// Tests...
})
Detection:
Fix:
beforeAll() for expensive one-time setupProblem: Weak or missing assertions that don't verify behavior.
// ❌ BAD: No assertion
test('creates user', async () => {
await createUser({ name: 'John' })
// No verification!
})
// ❌ BAD: Weak assertion
test('returns users', async () => {
const users = await getUsers()
expect(users).toBeDefined() // Too vague!
})
// ❌ BAD: Assertion on mock
test('calls API', async () => {
const mockAPI = vi.fn()
await service.fetchData(mockAPI)
expect(mockAPI).toHaveBeenCalled() // Testing mock, not behavior
})
// ✅ GOOD: Strong, specific assertions
test('creates user with correct attributes', async () => {
const user = await createUser({ name: 'John', email: 'john@example.com' })
expect(user).toMatchObject({
id: expect.any(Number),
name: 'John',
email: 'john@example.com',
createdAt: expect.any(Date),
})
})
Detection:
toBeDefined(), toBeTruthy())Fix:
Problem: Critical code paths not tested.
// Source code
function calculateDiscount(price: number, coupon?: string): number {
if (coupon === 'SAVE20') return price * 0.8
if (coupon === 'SAVE50') return price * 0.5
return price
}
// ❌ BAD: Only tests happy path
test('applies SAVE20 discount', () => {
expect(calculateDiscount(100, 'SAVE20')).toBe(80)
})
// ✅ GOOD: Tests all paths
describe('calculateDiscount', () => {
it('applies SAVE20 discount', () => {
expect(calculateDiscount(100, 'SAVE20')).toBe(80)
})
it('applies SAVE50 discount', () => {
expect(calculateDiscount(100, 'SAVE50')).toBe(50)
})
it('returns original price for invalid coupon', () => {
expect(calculateDiscount(100, 'INVALID')).toBe(100)
})
it('returns original price when no coupon provided', () => {
expect(calculateDiscount(100)).toBe(100)
})
})
Detection:
Fix:
# Vitest coverage
vitest --coverage
# View HTML report
open coverage/index.html
# Check thresholds
vitest --coverage --coverage.thresholds.lines=80
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
exclude: [
'node_modules/',
'**/*.config.ts',
'**/*.d.ts',
'**/types/**',
],
},
},
})
# pytest-cov
uv run pytest --cov --cov-report=html
# View report
open htmlcov/index.html
# Show missing lines
uv run pytest --cov --cov-report=term-missing
# Fail if below threshold
uv run pytest --cov --cov-fail-under=80
# pyproject.toml
[tool.coverage.run]
source = ["src"]
branch = true
omit = ["*/tests/*", "*/test_*.py"]
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
[tool.coverage.html]
directory = "htmlcov"
# Vitest: Show slow tests
vitest --reporter=verbose
# pytest: Show slowest tests
uv run pytest --durations=10
# pytest: Profile test execution
uv run pytest --profile
# Playwright: Trace for performance
npx playwright test --trace on
// ✅ GOOD: Descriptive test names
test('calculateTotal adds tax and shipping to subtotal', () => {})
test('login fails with invalid credentials', () => {})
test('createUser throws ValidationError for invalid email', () => {})
// ❌ BAD: Vague test names
test('test1', () => {})
test('works correctly', () => {})
test('handles error', () => {})
test('user registration flow', async () => {
// Arrange: Setup test data and dependencies
const userData = {
email: 'user@example.com',
password: 'secure123', // pragma: allowlist secret
}
const mockEmailService = vi.fn()
// Act: Execute the behavior being tested
const user = await registerUser(userData, mockEmailService)
// Assert: Verify the expected outcome
expect(user).toMatchObject({
email: 'user@example.com',
emailVerified: false,
})
expect(mockEmailService).toHaveBeenCalledWith(
'user@example.com',
expect.any(String)
)
})
When reviewing tests, check for:
sleep, setTimeout)beforeAll(), not beforeEach()// Before: Overmocked
test('processes order', () => {
const mockValidator = vi.fn(() => true)
const mockCalculator = vi.fn(() => 100)
const mockFormatter = vi.fn(() => '$100.00')
const result = processOrder(mockValidator, mockCalculator, mockFormatter)
expect(result).toBe('$100.00')
})
// After: Mock only I/O
test('processes order and sends confirmation', async () => {
const mockEmailService = vi.fn()
const order = { items: [{ price: 50 }, { price: 50 }] }
await processOrder(order, mockEmailService)
expect(mockEmailService).toHaveBeenCalledWith(
expect.objectContaining({
total: 100,
formattedTotal: '$100.00',
})
)
})
// Before: Flaky
test('animation completes', async () => {
triggerAnimation()
await new Promise(resolve => setTimeout(resolve, 500))
expect(isAnimationComplete()).toBe(true)
})
// After: Deterministic
test('animation completes', async () => {
vi.useFakeTimers()
triggerAnimation()
vi.advanceTimersByTime(500)
expect(isAnimationComplete()).toBe(true)
vi.restoreAllMocks()
})
// ❌ BAD
test('uses correct algorithm', () => {
const spy = vi.spyOn(Math, 'sqrt')
calculateDistance({ x: 0, y: 0 }, { x: 3, y: 4 })
expect(spy).toHaveBeenCalled() // Testing how, not what
})
// ✅ GOOD
test('calculates distance correctly', () => {
const distance = calculateDistance({ x: 0, y: 0 }, { x: 3, y: 4 })
expect(distance).toBe(5) // Testing output
})
// ❌ BAD: Mocking everything
const mockAdd = vi.fn((a, b) => a + b)
const mockMultiply = vi.fn((a, b) => a * b)
const mockFormat = vi.fn((n) => `$${n}`)
// ✅ GOOD: Use real implementations
import { add, multiply, format } from './utils'
// Only mock external services
const mockPaymentGateway = vi.fn()
// ❌ BAD: Skipping failing tests
test.skip('this test is broken', () => {
// Don't leave broken tests!
})
// ✅ GOOD: Fix or remove
test('feature works correctly', () => {
// Fixed implementation
})
# Run tests with coverage
vitest --coverage
# Find slow tests
vitest --reporter=verbose
# Watch mode for TDD
vitest --watch
# UI mode for debugging
vitest --ui
# Generate coverage report
vitest --coverage --coverage.reporter=html
# Run tests with coverage
uv run pytest --cov
# Show missing lines
uv run pytest --cov --cov-report=term-missing
# Find slow tests
uv run pytest --durations=10
# Run only failed tests
uv run pytest --lf
# Generate HTML coverage report
uv run pytest --cov --cov-report=html
vitest-testing - TypeScript/JavaScript testingpython-testing - Python pytest testingplaywright-testing - E2E testingmutation-testing - Validate test effectivenessThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.