MCPInstall

testing-strategies

Unit, integration, E2E testing and TDD practices

Installs
88
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/miles990/claude-software-skills --skill testing-strategies
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads testing-strategies automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Testing Strategies

Overview

Testing pyramid, patterns, and practices for building reliable software.


Testing Pyramid

           /\
          /  \
         / E2E\        Few, slow, expensive
        /──────\
       /        \
      /Integration\    Some, medium speed
     /──────────────\
    /                \
   /    Unit Tests    \  Many, fast, cheap
  /____________________\
LevelSpeedScopeQuantity
UnitFast (ms)Single function/classMany (70%)
IntegrationMedium (s)Multiple componentsSome (20%)
E2ESlow (min)Full systemFew (10%)

Unit Testing

Structure: Arrange-Act-Assert

describe('calculateDiscount', () => {
  it('applies 10% discount for orders over $100', () => {
    // Arrange
    const order = { items: [{ price: 150 }] };
    const discountService = new DiscountService();

    // Act
    const result = discountService.calculateDiscount(order);

    // Assert
    expect(result).toBe(15);
  });

  it('returns 0 for orders under $100', () => {
    // Arrange
    const order = { items: [{ price: 50 }] };
    const discountService = new DiscountService();

    // Act
    const result = discountService.calculateDiscount(order);

    // Assert
    expect(result).toBe(0);
  });
});

Mocking

// Mock dependencies
const mockEmailService = {
  send: jest.fn().mockResolvedValue({ success: true })
};

const mockUserRepo = {
  findById: jest.fn().mockResolvedValue({ id: '1', email: 'test@example.com' })
};

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

  beforeEach(() => {
    jest.clearAllMocks();
    service = new NotificationService(mockEmailService, mockUserRepo);
  });

  it('sends email to user', async () => {
    await service.notifyUser('1', 'Hello!');

    expect(mockUserRepo.findById).toHaveBeenCalledWith('1');
    expect(mockEmailService.send).toHaveBeenCalledWith(
      'test@example.com',
      'Hello!'
    );
  });

  it('throws when user not found', async () => {
    mockUserRepo.findById.mockResolvedValue(null);

    await expect(service.notifyUser('999', 'Hello!'))
      .rejects.toThrow('User not found');
  });
});

Testing Edge Cases

describe('parseAge', () => {
  // Happy path
  it('parses valid age string', () => {
    expect(parseAge('25')).toBe(25);
  });

  // Edge cases
  it('handles zero', () => {
    expect(parseAge('0')).toBe(0);
  });

  it('handles boundary values', () => {
    expect(parseAge('1')).toBe(1);
    expect(parseAge('150')).toBe(150);
  });

  // Error cases
  it('throws on negative numbers', () => {
    expect(() => parseAge('-5')).toThrow('Age cannot be negative');
  });

  it('throws on non-numeric input', () => {
    expect(() => parseAge('abc')).toThrow('Invalid age format');
  });

  it('throws on empty string', () => {
    expect(() => parseAge('')).toThrow('Age is required');
  });

  // Null/undefined
  it('throws on null', () => {
    expect(() => parseAge(null as any)).toThrow();
  });
});

Integration Testing

API Testing

import request from 'supertest';
import { app } from '../app';
import { db } from '../database';

describe('POST /api/users', () => {
  beforeEach(async () => {
    await db.users.deleteMany({});
  });

  afterAll(async () => {
    await db.disconnect();
  });

  it('creates a new user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'test@example.com',
        name: 'Test User'
      })
      .expect(201);

    expect(response.body).toMatchObject({
      id: expect.any(String),
      email: 'test@example.com',
      name: 'Test User'
    });

    // Verify in database
    const user = await db.users.findOne({ email: 'test@example.com' });
    expect(user).not.toBeNull();
  });

  it('returns 400 for invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'invalid-email',
        name: 'Test User'
      })
      .expect(400);

    expect(response.body.error).toBe('Invalid email format');
  });

  it('returns 409 for duplicate email', async () => {
    // Create first user
    await request(app)
      .post('/api/users')
      .send({ email: 'test@example.com', name: 'First' });

    // Try to create duplicate
    const response = await request(app)
      .post('/api/users')
      .send({ email: 'test@example.com', name: 'Second' })
      .expect(409);

    expect(response.body.error).toBe('Email already exists');
  });
});

Database Testing with Testcontainers

import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { Pool } from 'pg';

describe('UserRepository', () => {
  let container: StartedPostgreSqlContainer;
  let pool: Pool;
  let repo: UserRepository;

  beforeAll(async () => {
    container = await new PostgreSqlContainer().start();
    pool = new Pool({ connectionString: container.getConnectionUri() });
    await runMigrations(pool);
    repo = new UserRepository(pool);
  }, 60000);

  afterAll(async () => {
    await pool.end();
    await container.stop();
  });

  beforeEach(async () => {
    await pool.query('TRUNCATE users CASCADE');
  });

  it('creates and retrieves user', async () => {
    const created = await repo.create({
      email: 'test@example.com',
      name: 'Test'
    });

    const found = await repo.findById(created.id);

    expect(found).toEqual(created);
  });
});

E2E Testing

Playwright

import { test, expect } from '@playwright/test';

test.describe('User Authentication', () => {
  test('successful login flow', async ({ page }) => {
    await page.goto('/login');

    // Fill form
    await page.fill('[data-testid="email-input"]', 'user@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');

    // Submit
    await page.click('[data-testid="login-button"]');

    // Verify redirect to dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="welcome-message"]'))
      .toContainText('Welcome, user@example.com');
  });

  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login');

    await page.fill('[data-testid="email-input"]', 'wrong@example.com');
    await page.fill('[data-testid="password-input"]', 'wrongpassword');
    await page.click('[data-testid="login-button"]');

    await expect(page.locator('[data-testid="error-message"]'))
      .toBeVisible()
      .toContainText('Invalid credentials');
  });
});

test.describe('Shopping Cart', () => {
  test('add item and checkout', async ({ page }) => {
    // Setup - login
    await page.goto('/login');
    await page.fill('[data-testid="email-input"]', 'buyer@example.com');
    await page.fill('[data-testid="password-input"]', 'password');
    await page.click('[data-testid="login-button"]');

    // Browse products
    await page.goto('/products');
    await page.click('[data-testid="product-1"] [data-testid="add-to-cart"]');

    // Verify cart
    await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');

    // Checkout
    await page.click('[data-testid="cart-icon"]');
    await page.click('[data-testid="checkout-button"]');

    // Fill shipping
    await page.fill('[data-testid="address"]', '123 Test St');
    await page.click('[data-testid="place-order"]');

    // Verify success
    await expect(page).toHaveURL(/\/orders\/\d+/);
    await expect(page.locator('[data-testid="order-status"]'))
      .toContainText('Order Confirmed');
  });
});

Visual Regression Testing

import { test, expect } from '@playwright/test';

test('homepage visual regression', async ({ page }) => {
  await page.goto('/');

  // Wait for dynamic content
  await page.waitForSelector('[data-testid="hero-section"]');

  // Take screenshot and compare
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixels: 100,
    threshold: 0.2
  });
});

test('responsive design', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
  await page.goto('/');

  await expect(page).toHaveScreenshot('homepage-mobile.png');
});

Test-Driven Development (TDD)

Red-Green-Refactor Cycle

// 1. RED - Write failing test first
test('passwordValidator rejects passwords without numbers', () => {
  const result = validatePassword('NoNumbers!');
  expect(result.valid).toBe(false);
  expect(result.errors).toContain('Must contain at least one number');
});

// 2. GREEN - Write minimal code to pass
function validatePassword(password: string): ValidationResult {
  const errors: string[] = [];

  if (!/\d/.test(password)) {
    errors.push('Must contain at least one number');
  }

  return { valid: errors.length === 0, errors };
}

// 3. REFACTOR - Improve code quality
const VALIDATION_RULES = [
  { pattern: /\d/, message: 'Must contain at least one number' },
  { pattern: /[A-Z]/, message: 'Must contain at least one uppercase letter' },
  { pattern: /[a-z]/, message: 'Must contain at least one lowercase letter' },
  { pattern: /.{8,}/, message: 'Must be at least 8 characters' }
];

function validatePassword(password: string): ValidationResult {
  const errors = VALIDATION_RULES
    .filter(rule => !rule.pattern.test(password))
    .map(rule => rule.message);

  return { valid: errors.length === 0, errors };
}

Testing Patterns

Test Fixtures

// fixtures/users.ts
export const validUser = {
  email: 'test@example.com',
  name: 'Test User',
  role: 'user'
};

export const adminUser = {
  ...validUser,
  role: 'admin',
  email: 'admin@example.com'
};

// In tests
import { validUser, adminUser } from '../fixtures/users';

describe('UserService', () => {
  it('creates user with valid data', async () => {
    const result = await service.create(validUser);
    expect(result.email).toBe(validUser.email);
  });
});

Factory Functions

// factories/user.factory.ts
import { faker } from '@faker-js/faker';

export function createUser(overrides: Partial<User> = {}): User {
  return {
    id: faker.string.uuid(),
    email: faker.internet.email(),
    name: faker.person.fullName(),
    createdAt: faker.date.past(),
    ...overrides
  };
}

// In tests
it('handles users with long names', () => {
  const user = createUser({ name: 'A'.repeat(100) });
  const result = formatUserCard(user);
  expect(result.displayName).toHaveLength(50); // Truncated
});

Testing Async Code

// Async/await
it('fetches user data', async () => {
  const user = await userService.getById('123');
  expect(user.name).toBe('John');
});

// Promises
it('fetches user data', () => {
  return userService.getById('123').then(user => {
    expect(user.name).toBe('John');
  });
});

// Testing rejected promises
it('throws on invalid id', async () => {
  await expect(userService.getById('invalid'))
    .rejects.toThrow('User not found');
});

// Waiting for side effects
it('debounces search input', async () => {
  const onSearch = jest.fn();
  render(<SearchBox onSearch={onSearch} debounceMs={300} />);

  await userEvent.type(screen.getByRole('textbox'), 'test');

  // Should not have called yet
  expect(onSearch).not.toHaveBeenCalled();

  // Wait for debounce
  await waitFor(() => {
    expect(onSearch).toHaveBeenCalledWith('test');
  }, { timeout: 500 });
});

Code Coverage

Coverage Metrics

MetricWhat It Measures
LinePercentage of lines executed
BranchPercentage of if/else branches taken
FunctionPercentage of functions called
StatementPercentage of statements executed

Jest Configuration

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.stories.tsx',
    '!src/test/**'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Related Skills

  • [[code-quality]] - Writing testable code
  • [[devops-cicd]] - CI integration
  • [[performance-optimization]] - Performance testing

Sharp Edges(常見陷阱)

這些是測試中最常見且代價最高的錯誤

SE-1: 測試實作而非行為

  • 嚴重度: high
  • 情境: 測試過度耦合內部實作,重構時測試全部壞掉
  • 原因: 測試私有方法、mock 太細、驗證內部狀態
  • 症狀:
    • 改了一行程式碼,10 個測試失敗
    • 測試檔案比程式碼還長
    • 重構時花更多時間修測試
  • 檢測: expect.*\.toHaveBeenCalledTimes\(\d{2,}\)|mock.*private|spy.*internal
  • 解法: 測試公開 API/行為、使用 black-box testing、減少 mock 數量

SE-2: 假陽性測試 (False Positive)

  • 嚴重度: critical
  • 情境: 測試永遠通過,但實際上沒有驗證任何東西
  • 原因: 忘記 await、expect 沒有執行、條件判斷錯誤
  • 症狀:
    • 測試通過但 bug 仍然存在
    • 刪掉測試中的關鍵 assertion 測試還是通過
    • Coverage 高但信心低
  • 檢測: it\(.*\{\s*\}\)|expect\(.*\)(?!\.)|\.resolves(?!\.)|\.rejects(?!\.)
  • 解法: TDD(先寫失敗的測試)、review 測試程式碼、使用 ESLint no-floating-promises

SE-3: Flaky Tests(不穩定測試)

  • 嚴重度: high
  • 情境: 測試有時通過有時失敗,沒有程式碼變更
  • 原因: 依賴時間、依賴外部服務、競態條件、共享狀態
  • 症狀:
    • CI 需要 retry 才能通過
    • 本地通過但 CI 失敗
    • 團隊開始忽略失敗的測試
  • 檢測: new Date\(\)|Date\.now\(\)|setTimeout.*\d{4,}|sleep\(\d+\)
  • 解法: 使用 fake timers、隔離測試狀態、避免 hard-coded delays、mock 外部依賴

SE-4: 測試金字塔倒置

  • 嚴重度: medium
  • 情境: E2E 測試太多,單元測試太少,CI 超慢
  • 原因: 「E2E 測試更接近真實」的誤解、不想寫單元測試
  • 症狀:
    • CI 跑 30+ 分鐘
    • 測試失敗難以定位問題
    • E2E 測試經常 flaky
  • 檢測: describe.*E2E|playwright.*test|cypress.*it (數量遠超 unit test)
  • 解法: 遵循 70% unit / 20% integration / 10% E2E 比例、E2E 只測關鍵路徑

SE-5: 過度 Mocking

  • 嚴重度: medium
  • 情境: Mock 太多導致測試失去意義,只是在測試 mock
  • 原因: 為了隔離而 mock 所有依賴、測試執行時間焦慮
  • 症狀:
    • 測試通過但整合時失敗
    • Mock 的行為與真實行為不符
    • 更新依賴後 mock 過時
  • 檢測: jest\.mock.*jest\.mock.*jest\.mock|mock\(.*\).*mock\(.*\).*mock\(
  • 解法: 只 mock 外部依賴(網路、檔案系統)、使用真實的 in-memory 實作、寫更多整合測試

Validations

V-1: 禁止空的測試

  • 類型: regex
  • 嚴重度: critical
  • 模式: (it|test)\s*\([^)]+,\s*(async\s*)?\(\)\s*=>\s*\{\s*\}\s*\)
  • 訊息: Empty test detected - test has no assertions
  • 修復建議: Add meaningful assertions with expect()
  • 適用: *.test.ts, *.test.js, *.spec.ts, *.spec.js

V-2: 測試缺少 assertion

  • 類型: regex
  • 嚴重度: high
  • 模式: (it|test)\s*\([^)]+,\s*(async\s*)?\(\)\s*=>\s*\{[^}]*\}(?![^}]*expect)
  • 訊息: Test without expect() assertion may be a false positive
  • 修復建議: Add at least one expect() assertion
  • 適用: *.test.ts, *.test.js, *.spec.ts, *.spec.js

V-3: 禁止 fit/fdescribe (focused tests)

  • 類型: regex
  • 嚴重度: critical
  • 模式: \b(fit|fdescribe|it\.only|describe\.only|test\.only)\s*\(
  • 訊息: Focused test will skip other tests in CI
  • 修復建議: Remove f prefix or .only before committing
  • 適用: *.test.ts, *.test.js, *.spec.ts, *.spec.js

V-4: 禁止 skip tests 無說明

  • 類型: regex
  • 嚴重度: medium
  • 模式: (xit|xdescribe|it\.skip|describe\.skip|test\.skip)\s*\([^)]+\)
  • 訊息: Skipped test without documented reason
  • 修復建議: Add comment explaining why test is skipped and tracking issue
  • 適用: *.test.ts, *.test.js, *.spec.ts, *.spec.js

V-5: 測試中使用 setTimeout

  • 類型: regex
  • 嚴重度: high
  • 模式: setTimeout\s*\(\s*[^,]+,\s*\d{3,}\s*\)
  • 訊息: Hard-coded delays in tests cause flakiness and slow tests
  • 修復建議: Use jest.useFakeTimers() or waitFor() from testing-library
  • 適用: *.test.ts, *.test.js, *.spec.ts, *.spec.js

FAQ

What does the testing-strategies skill do?

Unit, integration, E2E testing and TDD practices

How do I install testing-strategies?

Run npx skills add https://github.com/miles990/claude-software-skills --skill testing-strategies in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/testing-strategies/ (personal) or .claude/skills/testing-strategies/ (project).

Which AI agents can use testing-strategies?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is testing-strategies safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains testing-strategies?

It is published by miles990 in miles990/claude-software-skills. It has 88 recorded installs.

More from miles990/claude-software-skills

View all →

code-quality

miles990/claude-software-skills

Clean code principles, SOLID, and code review practices

devops-cicd

miles990/claude-software-skills

CI/CD pipelines, infrastructure as code, and deployment strategies

DevOps & Cloud606 installs

game-development

miles990/claude-software-skills

Game development patterns, architectures, and best practices

Media & Creative411 installs

project-management

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

389 installs

e-commerce

miles990/claude-software-skills

E-commerce platforms, payment processing, and shopping cart patterns

Backend & APIs376 installs

saas-platforms

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

362 installs

Popular testing & qa skills

View category →

tdd

mattpocock/skills

Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.

Testing & QA1.2M installs

grilling

mattpocock/skills

Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.

Testing & QA996.2K installs

migrate-to-shoehorn

mattpocock/skills

Migrate test files from as type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace as in tests, or needs partial test data.

Testing & QA484.1K installs

run-train

lllllllama/rigorpilot-skills

Rigor Train skill for deep learning research repositories. Use when a documented or selected training command should be run conservatively for startup verification, short-run verification, full kickoff, or resume, with command, config, seed, log, checkpoint, status, and metric evidence written to standardized trainoutputs/. Do not use for environment setup, exploratory sweeps, speculative idea implementation, or end-to-end orchestration.

Testing & QA337.8K installs

test-driven-development

obra/superpowers

Use when implementing any feature or bugfix, before writing implementation code

Testing & QA261.8K installs

Use when implementation is complete, all tests pass, and you need to decide how to integrate the work

Testing & QA216.8K installs