AgentSkillsCN

qa-testing

为边缘场景、手动检查与验证流程提供 QA 与测试指导。

SKILL.md
--- frontmatter
name: qa-testing
description: QA and testing guidance for edge cases, manual checks, and validation flows.
argument-hint: Ask for test plans or edge case checklists.

QA and Testing

Use this skill to create comprehensive test plans, identify edge cases, and verify quality before release.


Table of Contents

  1. Testing Strategy
  2. Test Types
  3. Test Case Design
  4. Edge Cases
  5. Manual Testing
  6. Automation Strategy
  7. Quality Metrics
  8. Anti-Patterns to Avoid

Testing Strategy

Testing Pyramid

code
         /\
        /E2E\         <- Few (slow, expensive)
       /------\
      /  INT   \      <- Some (medium speed/cost)
     /----------\
    /   UNIT     \    <- Many (fast, cheap)
   /--------------\

Guidelines:

  • 70% Unit Tests - Fast, isolate logic, high coverage
  • 20% Integration Tests - Component + service layer + API mocks
  • 10% E2E Tests - Critical user flows only

When to Write Each Type

ScenarioTest TypeExample
Pure functionUnitsimilarity(query, target)
API clientIntegration (mock HTTP)fetchUserProfile(id)
Component with APIIntegration (mock service)<SearchBox onSearch={...} />
Critical flowE2E (real browser)Login → Create Post → Logout
Regression bugUnit + IntegrationAdd test that reproduces the bug

Test Types

Unit Tests

Purpose: Test individual functions/utilities in isolation.

typescript
// utils/similarity.test.ts
import { describe, it, expect } from "vitest";
import { calculateSimilarity } from "./similarity";

describe("calculateSimilarity", () => {
  it("returns 1.0 for exact matches", () => {
    expect(calculateSimilarity("hello", "hello")).toBe(1.0);
  });

  it("handles case-insensitive matches", () => {
    expect(calculateSimilarity("Hello", "HELLO")).toBe(1.0);
  });

  it("returns 0 for completely different strings", () => {
    expect(calculateSimilarity("abc", "xyz")).toBe(0);
  });

  it("handles partial matches", () => {
    const score = calculateSimilarity("hello world", "hello");
    expect(score).toBeGreaterThan(0);
    expect(score).toBeLessThan(1);
  });

  it("handles empty strings", () => {
    expect(calculateSimilarity("", "")).toBe(0);
    expect(calculateSimilarity("hello", "")).toBe(0);
  });

  it("handles special characters", () => {
    expect(calculateSimilarity("user@example.com", "user@example.com")).toBe(
      1.0,
    );
  });
});

Integration Tests

Purpose: Test components with dependencies (services, hooks, context).

typescript
// components/SearchBox.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import SearchBox from './SearchBox';

describe('SearchBox', () => {
  it('renders with placeholder text', () => {
    render(<SearchBox onSearch={vi.fn()} />);
    expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument();
  });

  it('calls onSearch when form submitted', async () => {
    const onSearch = vi.fn();
    render(<SearchBox onSearch={onSearch} />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'react' } });

    const button = screen.getByRole('button', { name: /search/i });
    fireEvent.click(button);

    await waitFor(() => {
      expect(onSearch).toHaveBeenCalledWith('react');
    });
  });

  it('shows loading state during search', async () => {
    const slowSearch = vi.fn(() => new Promise(resolve => setTimeout(resolve, 100)));
    render(<SearchBox onSearch={slowSearch} />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'test' } });

    const button = screen.getByRole('button', { name: /search/i });
    fireEvent.click(button);

    // Button should be disabled and show loading
    expect(button).toBeDisabled();
    expect(screen.getByText(/searching/i)).toBeInTheDocument();

    await waitFor(() => {
      expect(button).not.toBeDisabled();
    });
  });

  it('displays error message on failure', async () => {
    const failingSearch = vi.fn(() => Promise.reject(new Error('API Error')));
    render(<SearchBox onSearch={failingSearch} />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'test' } });

    const button = screen.getByRole('button', { name: /search/i });
    fireEvent.click(button);

    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
    });
  });

  it('prevents empty searches', () => {
    const onSearch = vi.fn();
    render(<SearchBox onSearch={onSearch} />);

    const button = screen.getByRole('button', { name: /search/i });
    fireEvent.click(button);

    expect(onSearch).not.toHaveBeenCalled();
  });
});

E2E Tests

Purpose: Test complete user flows in a real browser.

typescript
// tests/e2e/search-flow.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Search Flow", () => {
  test("user can search and view results", async ({ page }) => {
    // Navigate
    await page.goto("http://localhost:3000");

    // Search
    const searchInput = page.getByPlaceholder("Search...");
    await searchInput.fill("typescript");
    await searchInput.press("Enter");

    // Wait for results
    await expect(page.getByTestId("results-list")).toBeVisible();

    // Verify results contain search term
    const results = page.getByTestId("result-item");
    await expect(results.first()).toContainText("typescript", {
      ignoreCase: true,
    });
  });

  test("shows empty state when no results", async ({ page }) => {
    await page.goto("http://localhost:3000");

    const searchInput = page.getByPlaceholder("Search...");
    await searchInput.fill("xyzabc999nonexistent");
    await searchInput.press("Enter");

    await expect(page.getByText(/no results found/i)).toBeVisible();
  });

  test("keyboard navigation works", async ({ page }) => {
    await page.goto("http://localhost:3000");

    // Tab to search input
    await page.keyboard.press("Tab");
    await expect(page.getByPlaceholder("Search...")).toBeFocused();

    // Type and submit with Enter
    await page.keyboard.type("react");
    await page.keyboard.press("Enter");

    await expect(page.getByTestId("results-list")).toBeVisible();
  });
});

Test Case Design

Equivalence Partitioning

Divide inputs into groups that should behave the same way.

Example: Password validation

PartitionExampleExpected
Too short"abc"Invalid
Valid length"password123"Valid
Too long"a".repeat(200)Invalid
Empty""Invalid
Special chars"p@ssw0rd!"Valid

Boundary Value Analysis

Test at the edges of valid ranges.

typescript
// Age input (valid: 18-120)
describe("Age validation", () => {
  it("rejects 17 (below minimum)", () => {
    expect(validateAge(17)).toBe(false);
  });

  it("accepts 18 (minimum)", () => {
    expect(validateAge(18)).toBe(true);
  });

  it("accepts 120 (maximum)", () => {
    expect(validateAge(120)).toBe(true);
  });

  it("rejects 121 (above maximum)", () => {
    expect(validateAge(121)).toBe(false);
  });
});

State Transition Testing

Test all possible state changes.

typescript
// Modal states: closed → opening → open → closing → closed
test('modal state transitions', () => {
  const { rerender } = render(<Modal isOpen={false} />);
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // closed → open
  rerender(<Modal isOpen={true} />);
  expect(screen.getByRole('dialog')).toBeInTheDocument();

  // open → closed
  fireEvent.click(screen.getByLabelText('Close'));
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

Edge Cases

Always Test These

1. Empty States

typescript
test('handles empty array', () => {
  render(<List items={[]} />);
  expect(screen.getByText(/no items/i)).toBeInTheDocument();
});

2. Null/Undefined

typescript
test('handles missing data gracefully', () => {
  render(<UserProfile user={null} />);
  expect(screen.getByText(/loading/i)).toBeInTheDocument();
});

3. Large Data Sets

typescript
test('performance with 1000 items', () => {
  const items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
  const { container } = render(<VirtualList items={items} />);

  // Only rendered items should be in DOM (virtualization)
  const renderedItems = container.querySelectorAll('[data-item]');
  expect(renderedItems.length).toBeLessThan(50);
});

4. Special Characters

typescript
test("handles special characters in input", () => {
  const { result } = renderHook(() => useSearch());
  act(() => {
    result.current.search("user@example.com");
  });
  expect(result.current.results).toBeDefined();
});

5. Rate Limiting

typescript
test("respects rate limit", async () => {
  const api = createMockAPI({ rateLimit: 10 });

  // Make 11 requests
  const promises = Array.from({ length: 11 }, () => api.fetch("/data"));
  const results = await Promise.allSettled(promises);

  const failed = results.filter((r) => r.status === "rejected");
  expect(failed).toHaveLength(1);
});

6. Network Failures

typescript
test('handles network timeout', async () => {
  global.fetch = vi.fn(() => new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 100)
  ));

  render(<DataComponent />);

  await waitFor(() => {
    expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
  });
});

Manual Testing

Pre-Release Checklist

Functional:

  • All primary user flows work
  • Forms submit and validate correctly
  • Navigation works (back button, links)
  • Authentication flows (login, logout, session)
  • Error messages are user-friendly

Cross-Browser:

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

Responsive:

  • Mobile (320px width)
  • Tablet (768px width)
  • Desktop (1920px width)

Accessibility:

  • Keyboard navigation (Tab, Enter, Esc)
  • Focus indicators visible
  • Screen reader (VoiceOver/NVDA)
  • Color contrast (WCAG AA 4.5:1)

Performance:

  • No console errors
  • Page loads < 3 seconds
  • Interactions feel instant (< 100ms)
  • No memory leaks (check DevTools Memory)

Exploratory Testing

Technique: Freestyle testing without a script to discover unexpected issues.

Session Template:

markdown
## Exploratory Testing Session

**Feature:** Advanced Search
**Tester:** [Name]
**Duration:** 30 minutes
**Date:** 2026-02-13

### Test Charter

Explore search functionality focusing on filters, edge cases, and performance.

### Notes

- Tried searching with emoji: 💀 - returned 0 results (expected)
- Filter combinations work but slow with >100 results
- Back button doesn't preserve search state ⚠️
- Search works with keyboard only ✅

### Bugs Found

1. **Back button clears search** - Steps to reproduce: Search → Click result → Back → Search cleared
2. **Performance**: Search with 3 filters takes 2s (expected <500ms)

### Questions

- Should search history be saved?
- What's the max result limit?

Automation Strategy

What to Automate

Test TypeAutomate?Reason
Happy path✅ YesCore flows, run often
Regression bugs✅ YesPrevent re-introduction
Edge cases✅ YesEasy to forget manually
Visual design⚠️ PartialUse screenshot tests
Performance✅ YesHard to measure manually
Exploratory❌ NoRequires human creativity

CI/CD Integration

yaml
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: "pnpm"

      - run: pnpm install
      - run: pnpm lint
      - run: pnpm test:unit # Fast unit tests
      - run: pnpm test:integration # Integration tests
      - run: pnpm build # Type check + build

  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v3
        with:
          node-version: 20
          cache: "pnpm"

      - run: pnpm install
      - run: pnpm build
      - run: pnpm test:e2e # Slower E2E tests

Quality Metrics

Code Coverage Targets

typescript
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: "v8",
      thresholds: {
        lines: 80, // 80% line coverage
        functions: 80, // 80% function coverage
        branches: 75, // 75% branch coverage
        statements: 80, // 80% statement coverage
      },
    },
  },
});

Guidelines:

  • Utilities: 95%+ coverage (pure functions, easy to test)
  • Components: 80%+ coverage (UI logic, harder to test)
  • E2E: Cover 5-10 critical flows (not measured by coverage)

Test Quality Metrics

MetricTargetPurpose
Test Execution Time< 2 minutesFast feedback loop
Flaky Test Rate< 1%Reliable CI/CD
Bug Escape Rate< 5%Bugs caught pre-release
Test-to-Code Ratio1:1 to 2:1Sufficient test coverage

Anti-Patterns to Avoid

❌ Testing Implementation Details

typescript
// ❌ BAD: Testing internal state
test('updates count state', () => {
  const { result } = renderHook(() => useCounter());
  expect(result.current.count).toBe(0); // Testing internal state
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

// ✅ GOOD: Testing behavior
test('displays correct count after increment', () => {
  render(<Counter />);
  expect(screen.getByText('0')).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('1')).toBeInTheDocument();
});

❌ Brittle Selectors

typescript
// ❌ BAD: Fragile selectors
const button = container.querySelector(".px-4.py-2.bg-blue-500");

// ✅ GOOD: Semantic queries
const button = screen.getByRole("button", { name: /submit/i });

❌ Not Testing Edge Cases

typescript
// ❌ BAD: Only testing happy path
test('search works', () => {
  render(<Search />);
  fireEvent.change(input, { target: { value: 'react' } });
  expect(results).toHaveLength(5);
});

// ✅ GOOD: Test edge cases
test('search handles empty query', () => { /* ... */ });
test('search handles special characters', () => { /* ... */ });
test('search handles no results', () => { /* ... */ });
test('search handles API errors', () => { /* ... */ });

❌ Slow Tests

typescript
// ❌ BAD: Unnecessary waits
test('button click', async () => {
  render(<Button />);
  fireEvent.click(button);
  await new Promise(resolve => setTimeout(resolve, 1000)); // Arbitrary wait
});

// ✅ GOOD: Wait for specific condition
test('button click', async () => {
  render(<Button />);
  fireEvent.click(button);
  await waitFor(() => {
    expect(screen.getByText(/success/i)).toBeInTheDocument();
  });
});

QA Checklist

Before marking a feature "ready for release":

  • Unit tests written for new functions/utilities
  • Integration tests written for new components
  • E2E test added for critical user flow (if applicable)
  • Manual testing completed (cross-browser, responsive, accessibility)
  • Edge cases covered (empty, null, large data, errors)
  • No console errors in browser
  • Loading states tested
  • Error states tested
  • Performance acceptable (< 3s load, < 100ms interaction)
  • Coverage meets threshold (80%+ for components)
  • CI/CD passing (all tests green)

References


Skill Status: Enhanced Last Updated: 2026-02-13 (Phase 5)