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
- Fast: Milliseconds per test
- Isolated: No external dependencies
- Repeatable: Same result every run
- Self-Validating: Pass or fail clearly
- 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
- Test critical user journeys: Focus on business-critical paths
- Keep tests independent: Each test should be self-contained
- Use realistic data: Test with production-like data
- Handle async operations: Wait for elements, not time
- 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
- Race conditions: Async operations not properly awaited
- Shared state: Tests affecting each other
- Time dependencies: Tests relying on specific times
- External services: Network or service issues
- 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:
- Test Suite: Unit, integration, or E2E tests
- Test Plan: Comprehensive testing strategy document
- Test Fixtures: Factories and seed data
- Mocking Strategy: Mock implementations
- CI Configuration: Testing pipeline setup
- Coverage Report Analysis: Coverage improvement recommendations
- Flaky Test Debug: Root cause analysis
Weekly Installs
1
Repository
navedanan/backg…-removerFirst Seen
13 days ago
Security Audits
Installed on
windsurf1
amp1
cline1
opencode1
cursor1
kimi-cli1