QA and Testing
Use this skill to create comprehensive test plans, identify edge cases, and verify quality before release.
Table of Contents
- •Testing Strategy
- •Test Types
- •Test Case Design
- •Edge Cases
- •Manual Testing
- •Automation Strategy
- •Quality Metrics
- •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
| Scenario | Test Type | Example |
|---|---|---|
| Pure function | Unit | similarity(query, target) |
| API client | Integration (mock HTTP) | fetchUserProfile(id) |
| Component with API | Integration (mock service) | <SearchBox onSearch={...} /> |
| Critical flow | E2E (real browser) | Login → Create Post → Logout |
| Regression bug | Unit + Integration | Add 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
| Partition | Example | Expected |
|---|---|---|
| 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 Type | Automate? | Reason |
|---|---|---|
| Happy path | ✅ Yes | Core flows, run often |
| Regression bugs | ✅ Yes | Prevent re-introduction |
| Edge cases | ✅ Yes | Easy to forget manually |
| Visual design | ⚠️ Partial | Use screenshot tests |
| Performance | ✅ Yes | Hard to measure manually |
| Exploratory | ❌ No | Requires 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
| Metric | Target | Purpose |
|---|---|---|
| Test Execution Time | < 2 minutes | Fast feedback loop |
| Flaky Test Rate | < 1% | Reliable CI/CD |
| Bug Escape Rate | < 5% | Bugs caught pre-release |
| Test-to-Code Ratio | 1:1 to 2:1 | Sufficient 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
- •Testing Guidance - Setup and workflow
- •Troubleshooting - Common issues
- •Accessibility Skill - A11y testing
- •CASE-STUDIES.md - Testing Agent - Real-world examples
Skill Status: Enhanced Last Updated: 2026-02-13 (Phase 5)