Awesome-omni-skill testing-patterns

Vitest, Playwright, and testing strategies. Use when writing tests, setting up test infrastructure, or debugging test failures.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/testing-security/testing-patterns-erikpr1994" ~/.claude/skills/diegosouzapw-awesome-omni-skill-testing-patterns-00918a && rm -rf "$T"
manifest: skills/testing-security/testing-patterns-erikpr1994/SKILL.md
source content

Testing Patterns

Overview

Decision guide for testing strategies focusing on Vitest for unit/integration and Playwright for E2E tests.

Test Type Decision

Test TypeToolUse When
UnitVitestPure functions, utilities, hooks
IntegrationVitestComponents with dependencies
E2EPlaywrightCritical user flows
VisualPlaywrightUI regression

Testing Strategy

Strategy is auto-detected based on project type and can be configured per-directory.

Automatic Strategy Detection

Check current project strategy:

# Via detect.sh
source ~/.claude/lib/testing/strategy-detector.sh
detect_testing_strategy "."              # Project-level
detect_directory_strategy "." "src/lib"  # Directory-level

Or check

settings.json
testing.strategy
if explicitly set.

Per-Directory Strategy Rules

Directory PatternStrategyRationale
lib/**
,
packages/**
,
utils/**
PyramidPure functions, edge cases
src/components/**
,
app/**
TrophyUser-facing, integration
api/**
,
server/**
TrophyTest real queries
algorithms/**
,
core/**
PyramidComplex logic, fast feedback

Testing Pyramid (Traditional)

     /  E2E  \        Few - slow, brittle, high confidence
    /   Int   \       Some - moderate speed/confidence
   /   Unit    \      Many - fast, isolated, low confidence

Best for: Libraries, utilities, pure logic, microservices

Guidance:

  • Test every public function with unit tests
  • Cover edge cases extensively (null, empty, boundaries)
  • Mock external dependencies
  • Aim for >80% unit test coverage

Testing Trophy (Kent C. Dodds)

       E2E            Few - critical paths only
   Integration        MOST - best confidence/speed ratio
      Unit            Some - complex logic only
     Static           TypeScript, ESLint, etc.

Best for: React apps, user-facing features, full-stack apps

Guidance:

  • Test user workflows, not implementation details
  • Use Testing Library patterns (query by role, text)
  • Mock only network/external services
  • Unit test only complex business logic

When to Use Each

Project TypeAuto-DetectedReasoning
UI-heavy appTrophyIntegration tests catch real user issues
Pure libraryPyramidUnit tests cover edge cases efficiently
API/Backend with DBTrophyIntegration tests verify real queries
API/Backend purePyramidUnit + contract tests are faster
Full-stackTrophyIntegration through API boundaries
MonorepoBalancedPer-directory strategy applies

Override Strategy

Project-level - Add to

.claude/settings.json
:

{
  "testing": {
    "strategy": "pyramid"
  }
}

Directory-level - Add frontmatter to folder's

CLAUDE.md
:

---
testing_strategy: trophy
---

Vitest Patterns

Basic Test Structure

import { describe, it, expect, beforeEach, vi } from 'vitest';

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    service = new UserService();
    vi.clearAllMocks();
  });

  it('creates user with valid data', async () => {
    const user = await service.create({ email: 'test@example.com' });
    expect(user).toMatchObject({ email: 'test@example.com' });
  });

  it('throws on duplicate email', async () => {
    await service.create({ email: 'test@example.com' });
    await expect(service.create({ email: 'test@example.com' }))
      .rejects.toThrow('already exists');
  });
});

Mocking

// Mock module
vi.mock('@/lib/db', () => ({
  db: {
    user: {
      create: vi.fn(),
      findUnique: vi.fn(),
    },
  },
}));

// Mock implementation per test
it('handles not found', async () => {
  vi.mocked(db.user.findUnique).mockResolvedValue(null);
  await expect(getUser('123')).rejects.toThrow('Not found');
});

// Spy on existing function
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(spy).toHaveBeenCalledWith('Error message');

React Component Testing

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

it('submits form with valid data', async () => {
  const onSubmit = vi.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
  await userEvent.type(screen.getByLabelText(/password/i), 'password123');
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  await waitFor(() => {
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });
});

Playwright Patterns

Page Object Model

// tests/pages/login.page.ts
export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.fill('[name="email"]', email);
    await this.page.fill('[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }

  async expectError(message: string) {
    await expect(this.page.getByText(message)).toBeVisible();
  }
}

// tests/auth.spec.ts
test('user can login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password');
  await expect(page).toHaveURL('/dashboard');
});

Fixtures

// tests/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login.page';

type Fixtures = {
  loginPage: LoginPage;
  authenticatedPage: Page;
};

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('[name="email"]', 'test@example.com');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Running Long E2E Tests Efficiently

E2E tests often take 2-5+ minutes. Use the TaskOutput pattern to avoid token waste.

The Problem: Polling Waste

❌ INEFFICIENT - wastes tokens on empty polling:
1. Bash(test, run_in_background: true) → task_id
2. Bash(sleep 60 && tail output)  ← empty, wasted tokens
3. Bash(cat output)               ← still empty, wasted
4. Bash(ps aux | grep test)       ← check if running, wasted
5. Bash(sleep 90 && cat output)   ← wasted again
... repeat 5-10 times before getting results

The Solution: Block Until Complete

✅ EFFICIENT - one call, waits for completion:
1. Bash(test, run_in_background: true) → task_id
2. TaskOutput(task_id, block: true, timeout: 300000)
   ↳ Waits up to 5 min, returns results when done

That's it. Two tool calls instead of 6+.

Pattern: E2E Test Execution

// Step 1: Start test in background
Bash("pnpm test:e2e -- e2e/checkout.spec.ts", run_in_background: true)
// Returns: task_id = "abc123"

// Step 2: Wait for completion (up to 5 min)
TaskOutput(task_id: "abc123", block: true, timeout: 300000)
// Returns full output when test completes

Pattern: Parallel E2E with TaskOutput

// Start multiple tests in parallel
Bash("pnpm test:e2e auth.spec.ts", run_in_background: true)  // task_a
Bash("pnpm test:e2e cart.spec.ts", run_in_background: true)  // task_b
Bash("pnpm test:e2e checkout.spec.ts", run_in_background: true)  // task_c

// Wait for all to complete
TaskOutput(task_id: "task_a", block: true, timeout: 300000)
TaskOutput(task_id: "task_b", block: true, timeout: 300000)
TaskOutput(task_id: "task_c", block: true, timeout: 300000)

When to Use Non-Blocking

Use

block: false
only when you need a quick status check:

// Quick status check (doesn't wait)
TaskOutput(task_id: "abc123", block: false, timeout: 5000)

// Returns immediately with current output
// Use this if you want to continue other work

Timeout Guidelines

Test TypeSuggested Timeout
Single unit test file60000 (1 min)
Integration tests120000 (2 min)
E2E single spec300000 (5 min)
E2E full suite600000 (10 min)

Red Flags

  • Using
    sleep && tail
    to poll → Use TaskOutput instead
  • Multiple
    cat
    commands checking same file → Use TaskOutput with block: true
  • Checking
    ps aux | grep
    for process → TaskOutput tells you if still running

Anti-Patterns

Anti-PatternProblemSolution
Testing implementationBrittle testsTest behavior/output
No test isolationFlaky testsReset state in beforeEach
Hardcoded delaysSlow, flakyUse waitFor/polling
Testing third-party codeWasted effortMock at boundary
Snapshot abuseMeaningless diffsUse for specific UI
Polling background tasksToken wasteUse TaskOutput(block: true)
// BAD: Testing implementation
expect(component.state.isLoading).toBe(true);

// GOOD: Testing behavior
expect(screen.getByRole('status')).toHaveTextContent('Loading...');

// BAD: Hardcoded delay
await page.waitForTimeout(2000);

// GOOD: Wait for condition
await page.waitForSelector('[data-loaded="true"]');

Test Organization

tests/
├── unit/              # Pure function tests
├── integration/       # Component + dependency tests
├── e2e/              # Playwright tests
│   ├── fixtures/
│   ├── pages/        # Page objects
│   └── *.spec.ts
└── setup.ts          # Global setup

Red Flags

  • Tests that pass when code is broken
  • Tests that fail intermittently (flaky)
  • Tests longer than 50 lines (decompose)
  • Mocking everything (test becomes meaningless)
  • No assertions (test does nothing)
  • Duplicated setup across tests (use fixtures)

Quick Reference

// Vitest config
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    coverage: { reporter: ['text', 'html'] },
  },
});

// Playwright config
export default defineConfig({
  testDir: './tests/e2e',
  use: { baseURL: 'http://localhost:3000' },
  webServer: { command: 'npm run dev', port: 3000 },
});

// Fast feedback: watch mode
vitest --watch
playwright test --ui