testing-qa

SKILL.md

Testing & QA

Expert guidance for comprehensive software testing and quality assurance strategies.

When to Use This Skill

  • Writing unit, integration, or E2E tests
  • Creating test plans and strategies
  • Setting up testing frameworks
  • Implementing mock/stub strategies
  • Establishing test coverage goals
  • Debugging flaky tests
  • Setting up CI/CD testing pipelines
  • Performance and load testing

Testing Pyramid

                 ┌───────┐
                 │  E2E  │  Fewer, slower, more expensive
                 │ Tests │
                ─┴───────┴─
               ┌───────────┐
               │Integration│  More than E2E
               │   Tests   │
              ─┴───────────┴─
             ┌───────────────┐
             │  Unit Tests   │  Most tests, fastest, cheapest
             └───────────────┘
Type Scope Speed Cost Count
Unit Single function/class Fast Low Many
Integration Multiple components Medium Medium Some
E2E Full application Slow High Few

Unit Testing

Unit Test Principles

  1. Fast: Milliseconds per test
  2. Isolated: No external dependencies
  3. Repeatable: Same result every run
  4. Self-Validating: Pass or fail clearly
  5. Timely: Written with or before code

Test Structure (AAA Pattern)

describe('Calculator', () => {
  describe('add', () => {
    it('should add two positive numbers', () => {
      // Arrange
      const calculator = new Calculator();
      
      // Act
      const result = calculator.add(2, 3);
      
      // Assert
      expect(result).toBe(5);
    });
    
    it('should handle negative numbers', () => {
      // Arrange
      const calculator = new Calculator();
      
      // Act
      const result = calculator.add(-1, -2);
      
      // Assert
      expect(result).toBe(-3);
    });
  });
});

Test Naming Convention

// Format: should_expectedBehavior_when_stateUnderTest

// Good examples:
it('should return empty array when no items exist')
it('should throw ValidationError when email is invalid')
it('should calculate total including tax when cart has items')

// Bad examples:
it('test add')
it('works')
it('user test')

What to Unit Test

Do test:

  • Pure functions and calculations
  • Business logic and rules
  • Data transformations
  • Edge cases and boundaries
  • Error handling

Don't test:

  • Third-party libraries
  • Framework code
  • Private methods directly
  • Trivial getters/setters
  • Database queries (integration test)

Mocking Strategies

// Mock dependencies
const mockUserRepository = {
  findById: jest.fn(),
  save: jest.fn(),
};

// Stub return values
mockUserRepository.findById.mockResolvedValue({
  id: '123',
  name: 'John Doe',
  email: 'john@example.com',
});

// Spy on existing methods
const logSpy = jest.spyOn(console, 'log');

// Verify interactions
expect(mockUserRepository.findById).toHaveBeenCalledWith('123');
expect(mockUserRepository.findById).toHaveBeenCalledTimes(1);

Test Doubles Comparison

Type Description Example Use
Dummy Passed but never used Required parameter
Stub Returns predefined data Database query result
Spy Records calls for verification Logging verification
Mock Stub + expectations API client
Fake Working simplified implementation In-memory database

Integration Testing

Integration Test Scope

┌─────────────────────────────────────────────────────┐
│                 Integration Test                     │
│  ┌──────────────┐    ┌──────────────┐              │
│  │   Service    │───▶│   Database   │              │
│  └──────────────┘    └──────────────┘              │
│         │                                           │
│         ▼                                           │
│  ┌──────────────┐                                  │
│  │External API  │  (mocked at boundary)            │
│  └──────────────┘                                  │
└─────────────────────────────────────────────────────┘

Database Integration Tests

describe('UserRepository', () => {
  let connection: DatabaseConnection;
  let repository: UserRepository;
  
  beforeAll(async () => {
    // Set up test database
    connection = await createTestConnection();
    repository = new UserRepository(connection);
  });
  
  afterAll(async () => {
    await connection.close();
  });
  
  beforeEach(async () => {
    // Clean state before each test
    await connection.query('TRUNCATE users CASCADE');
  });
  
  it('should create and retrieve user', async () => {
    // Arrange
    const userData = { name: 'John', email: 'john@test.com' };
    
    // Act
    const created = await repository.create(userData);
    const retrieved = await repository.findById(created.id);
    
    // Assert
    expect(retrieved).toMatchObject(userData);
  });
});

API Integration Tests

describe('POST /api/users', () => {
  let app: Express;
  
  beforeAll(async () => {
    app = await createTestApp();
  });
  
  it('should create user with valid data', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'SecurePass123!',
      })
      .expect(201);
    
    expect(response.body.data).toHaveProperty('id');
    expect(response.body.data.email).toBe('john@example.com');
  });
  
  it('should return 422 with invalid email', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        name: 'John Doe',
        email: 'invalid-email',
        password: 'SecurePass123!',
      })
      .expect(422);
    
    expect(response.body.error.code).toBe('VALIDATION_ERROR');
  });
});

End-to-End Testing

E2E Test Principles

  1. Test critical user journeys: Focus on business-critical paths
  2. Keep tests independent: Each test should be self-contained
  3. Use realistic data: Test with production-like data
  4. Handle async operations: Wait for elements, not time
  5. Isolate from external services: Mock when necessary

Page Object Pattern

// pages/LoginPage.ts
class LoginPage {
  private page: Page;
  
  constructor(page: Page) {
    this.page = page;
  }
  
  async navigate() {
    await this.page.goto('/login');
  }
  
  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email"]', email);
    await this.page.fill('[data-testid="password"]', password);
    await this.page.click('[data-testid="submit"]');
  }
  
  async getErrorMessage(): Promise<string> {
    return this.page.textContent('[data-testid="error-message"]');
  }
}

// tests/login.spec.ts
describe('Login', () => {
  let loginPage: LoginPage;
  
  beforeEach(async () => {
    loginPage = new LoginPage(page);
    await loginPage.navigate();
  });
  
  it('should login with valid credentials', async () => {
    await loginPage.login('user@example.com', 'password123');
    await expect(page).toHaveURL('/dashboard');
  });
  
  it('should show error with invalid credentials', async () => {
    await loginPage.login('user@example.com', 'wrong');
    const error = await loginPage.getErrorMessage();
    expect(error).toContain('Invalid credentials');
  });
});

E2E Testing Best Practices

// Use data-testid for selectors (stable)
<button data-testid="submit-order">Submit</button>
await page.click('[data-testid="submit-order"]');

// Wait for elements, not time
await page.waitForSelector('[data-testid="success-message"]');

// Use assertion timeouts
await expect(page.locator('[data-testid="loading"]')).toBeHidden();

// Reset state before tests
beforeEach(async () => {
  await resetDatabase();
  await seedTestData();
});

Test Coverage

Coverage Metrics

Metric Description Target
Line coverage Lines executed > 80%
Branch coverage Decision paths > 75%
Function coverage Functions called > 90%
Statement coverage Statements executed > 80%

Coverage Strategy

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/index.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 75,
      functions: 90,
      lines: 80,
      statements: 80,
    },
    // Higher threshold for critical paths
    './src/services/payment/': {
      branches: 90,
      functions: 100,
      lines: 95,
    },
  },
};

Coverage Anti-Patterns

Don't chase 100%: Diminishing returns ❌ Don't test trivia: Focus on logic ❌ Don't ignore quality: High coverage ≠ good tests ❌ Don't game metrics: Tests should verify behavior

Test Data Management

Test Data Strategies

// Factory pattern
const userFactory = {
  build: (overrides = {}) => ({
    id: faker.datatype.uuid(),
    name: faker.name.fullName(),
    email: faker.internet.email(),
    createdAt: new Date(),
    ...overrides,
  }),
  
  buildMany: (count: number, overrides = {}) => 
    Array.from({ length: count }, () => userFactory.build(overrides)),
};

// Usage
const user = userFactory.build({ name: 'Specific Name' });
const users = userFactory.buildMany(10, { role: 'admin' });

Database Fixtures

// fixtures/users.ts
export const testUsers = {
  admin: {
    id: 'admin-001',
    email: 'admin@test.com',
    role: 'admin',
    password: 'hashed_password',
  },
  regularUser: {
    id: 'user-001',
    email: 'user@test.com',
    role: 'user',
    password: 'hashed_password',
  },
};

// Seed function
async function seedDatabase() {
  await db.users.createMany(Object.values(testUsers));
}

Test Plan Template

# Test Plan: [Feature Name]

## Overview
Brief description of what's being tested.

## Scope
### In Scope
- Feature A functionality
- Feature B functionality

### Out of Scope
- Feature C (tested separately)

## Test Strategy
| Type | Tools | Coverage Goal |
|------|-------|---------------|
| Unit | Jest | 85% |
| Integration | Supertest | API endpoints |
| E2E | Playwright | Critical paths |

## Test Scenarios

### Scenario 1: [Happy Path]
**Preconditions:** User is logged in
**Steps:**
1. Navigate to page
2. Fill form
3. Submit

**Expected Result:** Success message shown

### Scenario 2: [Error Case]
**Preconditions:** User is logged in
**Steps:**
1. Submit empty form

**Expected Result:** Validation errors displayed

## Test Data Requirements
- Test user accounts
- Sample data records

## Environment
- Test database
- Mocked external services

## Exit Criteria
- All critical paths tested
- No P1/P2 bugs open
- Coverage thresholds met

Continuous Testing

CI Pipeline Testing

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run test:unit -- --coverage
      - uses: codecov/codecov-action@v3
  
  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: test
          POSTGRES_PASSWORD: test
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:integration
  
  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright install
      - run: npm run test:e2e

Debugging Flaky Tests

Common Causes

  1. Race conditions: Async operations not properly awaited
  2. Shared state: Tests affecting each other
  3. Time dependencies: Tests relying on specific times
  4. External services: Network or service issues
  5. Resource constraints: Memory or CPU limits

Debugging Steps

// 1. Isolate the test
it.only('flaky test', async () => { ... });

// 2. Run multiple times
// npm test -- --repeat=100

// 3. Add logging
console.log('State before:', await getState());

// 4. Check for async issues
await expect(async () => {
  await operation();
}).not.toThrow();

// 5. Mock time if needed
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-01'));

Output Artifacts

When this skill is activated, I can help create:

  1. Test Suite: Unit, integration, or E2E tests
  2. Test Plan: Comprehensive testing strategy document
  3. Test Fixtures: Factories and seed data
  4. Mocking Strategy: Mock implementations
  5. CI Configuration: Testing pipeline setup
  6. Coverage Report Analysis: Coverage improvement recommendations
  7. Flaky Test Debug: Root cause analysis
Weekly Installs
1
First Seen
13 days ago
Installed on
windsurf1
amp1
cline1
opencode1
cursor1
kimi-cli1