| name | qa-engineer |
| description | Expert guidance for software testing and quality assurance. Use when the user asks to write tests, create test plans, review code for bugs, perform code reviews, write test cases, set up testing frameworks, debug issues, validate requirements, create bug reports, perform regression testing, or improve test coverage. Triggers on testing, QA, quality assurance, test cases, bug reports, test automation, unit tests, integration tests, E2E tests, test coverage, debugging, code review. |
QA Engineer & Software Testing Expert
Expert guidance for comprehensive software testing, quality assurance, and bug detection.
Testing Philosophy
Core Principles
- Shift left — Find bugs early; prevention over detection
- Risk-based testing — Prioritize high-impact, high-probability failure areas
- Test pyramid — Many unit tests, fewer integration tests, minimal E2E tests
- Automation first — Automate repetitive tests; manual for exploratory
- Clean test code — Tests are production code; maintain them accordingly
Test Pyramid Distribution
/\
/ \ E2E (5-10%)
/----\ - Critical user journeys
/ \
/--------\ Integration (15-25%)
/ \ - API contracts, DB interactions
/------------\
/ \ Unit (65-80%)
/________________\ - Functions, components, logic
Test Case Design
Structure (Arrange-Act-Assert)
describe('ShoppingCart', () => {
describe('addItem', () => {
it('should increase quantity when adding existing item', () => {
// Arrange
const cart = new ShoppingCart();
cart.addItem({ id: '1', name: 'Apple', quantity: 1 });
// Act
cart.addItem({ id: '1', name: 'Apple', quantity: 2 });
// Assert
expect(cart.getItem('1').quantity).toBe(3);
});
});
});
Naming Convention
[Unit]_[Scenario]_[ExpectedResult]
Examples:
- calculateTotal_withEmptyCart_returnsZero
- login_withInvalidPassword_showsErrorMessage
- submitOrder_whenOutOfStock_preventsCheckout
Test Case Categories
Positive Tests — Valid inputs produce expected outputs
it('should create user with valid email and password', async () => {
const user = await createUser('test@example.com', 'ValidPass123!');
expect(user.id).toBeDefined();
expect(user.email).toBe('test@example.com');
});
Negative Tests — Invalid inputs handled gracefully
it('should reject user creation with invalid email', async () => {
await expect(createUser('invalid-email', 'ValidPass123!'))
.rejects.toThrow('Invalid email format');
});
Boundary Tests — Edge cases at limits
it('should accept password with exactly 8 characters (minimum)', () => {
expect(() => validatePassword('Pass123!')).not.toThrow();
});
it('should reject password with 7 characters (below minimum)', () => {
expect(() => validatePassword('Pass12!')).toThrow();
});
Error Handling Tests — Failures fail gracefully
it('should handle network timeout gracefully', async () => {
mockApi.simulateTimeout();
const result = await fetchUserData('123');
expect(result.error).toBe('Request timed out. Please try again.');
expect(result.data).toBeNull();
});
Test Types & Frameworks
Unit Testing
JavaScript/TypeScript — Jest/Vitest
// Function to test
export function calculateDiscount(price: number, percentage: number): number {
if (percentage < 0 || percentage > 100) {
throw new Error('Invalid percentage');
}
return price * (1 - percentage / 100);
}
// Test file
import { calculateDiscount } from './pricing';
describe('calculateDiscount', () => {
it('applies 20% discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('handles 0% discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('handles 100% discount', () => {
expect(calculateDiscount(100, 100)).toBe(0);
});
it('throws on negative percentage', () => {
expect(() => calculateDiscount(100, -10)).toThrow('Invalid percentage');
});
it('handles decimal prices', () => {
expect(calculateDiscount(99.99, 10)).toBeCloseTo(89.99, 2);
});
});
React Components — React Testing Library
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter', () => {
it('renders initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
it('increments count on button click', () => {
render(<Counter initialCount={0} />);
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onChange callback when count changes', () => {
const handleChange = jest.fn();
render(<Counter initialCount={0} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(handleChange).toHaveBeenCalledWith(1);
});
});
Integration Testing
API Integration — Supertest
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';
describe('POST /api/users', () => {
beforeEach(async () => {
await db.clear('users');
});
it('creates user and returns 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', password: 'SecurePass123!' })
.expect(201);
expect(response.body.user.email).toBe('test@example.com');
expect(response.body.user.password).toBeUndefined(); // Not exposed
// Verify database state
const dbUser = await db.users.findByEmail('test@example.com');
expect(dbUser).toBeDefined();
});
it('returns 409 for duplicate email', async () => {
await db.users.create({ email: 'test@example.com', password: 'hash' });
await request(app)
.post('/api/users')
.send({ email: 'test@example.com', password: 'SecurePass123!' })
.expect(409);
});
});
Database Integration
describe('UserRepository', () => {
let repo: UserRepository;
beforeAll(async () => {
await setupTestDatabase();
repo = new UserRepository(testDb);
});
afterEach(async () => {
await testDb.clear('users');
});
afterAll(async () => {
await teardownTestDatabase();
});
it('persists and retrieves user correctly', async () => {
const created = await repo.create({ name: 'John', email: 'john@test.com' });
const retrieved = await repo.findById(created.id);
expect(retrieved).toMatchObject({
name: 'John',
email: 'john@test.com',
});
});
});
E2E Testing
Playwright
import { test, expect } from '@playwright/test';
test.describe('User Authentication', () => {
test('complete login flow', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]'))
.toContainText('Welcome back');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'user@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"]'))
.toContainText('Invalid credentials');
await expect(page).toHaveURL('/login');
});
});
Mobile E2E — Detox (React Native)
describe('Shopping List', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
it('should add item to shopping list', async () => {
await element(by.id('add-item-button')).tap();
await element(by.id('item-name-input')).typeText('Milk');
await element(by.id('item-quantity-input')).typeText('2');
await element(by.id('save-button')).tap();
await expect(element(by.text('Milk'))).toBeVisible();
await expect(element(by.text('2'))).toBeVisible();
});
it('should mark item as bought', async () => {
await element(by.id('item-checkbox-milk')).tap();
await expect(element(by.id('item-milk'))).toHaveToggleValue(true);
});
});
Mocking Strategies
Function Mocks
// Mock external service
jest.mock('../services/emailService', () => ({
sendEmail: jest.fn().mockResolvedValue({ success: true }),
}));
// Test with mock
it('sends welcome email on registration', async () => {
await registerUser({ email: 'test@example.com', password: 'Pass123!' });
expect(emailService.sendEmail).toHaveBeenCalledWith({
to: 'test@example.com',
template: 'welcome',
});
});
API Mocks — MSW (Mock Service Worker)
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'Test User' }));
}),
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.status(201), ctx.json({ id: '123', ...req.body }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
it('handles server error gracefully', async () => {
server.use(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(500));
})
);
const result = await fetchUser('123');
expect(result.error).toBe('Server error');
});
Time Mocks
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15T10:00:00Z'));
});
afterEach(() => {
jest.useRealTimers();
});
it('expires session after 30 minutes', () => {
const session = createSession();
jest.advanceTimersByTime(31 * 60 * 1000); // 31 minutes
expect(session.isExpired()).toBe(true);
});
Bug Report Template
## Bug Report: [Short descriptive title]
**Severity:** Critical | High | Medium | Low
**Priority:** P0 | P1 | P2 | P3
**Environment:** Production | Staging | Development
**Platform:** iOS 17.2 / Android 14 / Chrome 120 / etc.
### Summary
[One sentence description of the issue]
### Steps to Reproduce
1. Navigate to [page/screen]
2. Enter [specific data]
3. Click [button/action]
4. Observe [behavior]
### Expected Behavior
[What should happen]
### Actual Behavior
[What actually happens]
### Evidence
- Screenshots: [attached]
- Video: [link]
- Console logs: [attached]
- Network trace: [attached]
### Impact
[Who is affected and how severely]
### Workaround
[If any temporary solution exists]
### Additional Context
- First noticed: [date]
- Frequency: Always | Intermittent (X/10 attempts)
- Related issues: #123, #456
Test Plan Template
# Test Plan: [Feature/Release Name]
## Overview
**Objective:** [What we're testing]
**Scope:** [In scope / Out of scope]
**Timeline:** [Start date - End date]
## Test Strategy
### Test Levels
| Level | Coverage | Automation |
|-------------|----------|------------|
| Unit | 80%+ | 100% |
| Integration | Critical paths | 90% |
| E2E | Happy paths | 70% |
| Manual | Edge cases | N/A |
### Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Payment failures | Medium | Critical | Extra payment gateway tests |
| Data migration | Low | High | Rollback testing |
## Test Cases
### Functional Tests
- [ ] TC001: User can create account with valid data
- [ ] TC002: User cannot create account with duplicate email
- [ ] TC003: User receives verification email
...
### Non-Functional Tests
- [ ] Performance: Page load < 2s
- [ ] Security: SQL injection prevention
- [ ] Accessibility: WCAG 2.1 AA compliance
## Entry/Exit Criteria
**Entry:**
- [ ] Code complete and deployed to staging
- [ ] Test data prepared
- [ ] Test environment stable
**Exit:**
- [ ] All critical tests pass
- [ ] No P0/P1 bugs open
- [ ] Test coverage meets targets
- [ ] Sign-off from QA lead
Code Review Checklist
Functionality
- Code does what the ticket/PR describes
- Edge cases handled
- Error handling is appropriate
- No hardcoded values that should be configurable
Security
- No sensitive data logged or exposed
- Input validation present
- SQL/NoSQL injection prevented
- Authentication/authorization checked
Performance
- No N+1 queries
- Appropriate indexes used
- No memory leaks (event listeners cleaned up)
- Large lists virtualized
Maintainability
- Code is readable and self-documenting
- Complex logic has comments
- No duplicate code
- Functions are single-purpose
Testing
- Unit tests added for new logic
- Edge cases tested
- Tests are deterministic (no flaky tests)
- Mocks are appropriate
Coverage Strategies
Minimum Coverage Targets
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
'./src/critical/': {
branches: 90,
functions: 95,
lines: 95,
},
},
};
Coverage Commands
# Generate coverage report
npm test -- --coverage
# View HTML report
open coverage/lcov-report/index.html
# Check specific file
npm test -- --coverage --collectCoverageFrom="src/utils/pricing.ts"
Debugging Techniques
Systematic Debugging
- Reproduce — Confirm the bug consistently
- Isolate — Narrow down to smallest failing case
- Identify — Find the root cause (not symptoms)
- Fix — Apply minimal, targeted fix
- Verify — Confirm fix and no regressions
- Document — Add test to prevent recurrence
Debug Logging
// Temporary debug logging (remove before commit)
console.log('[DEBUG] Input:', JSON.stringify(input, null, 2));
console.log('[DEBUG] State before:', { ...state });
// ... operation
console.log('[DEBUG] State after:', { ...state });
Binary Search Debugging
// Comment out half the code to isolate issue
// If bug persists: problem in remaining half
// If bug disappears: problem in commented half
// Repeat until isolated
Performance Testing
Load Testing with k6
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp up
{ duration: '3m', target: 50 }, // Steady state
{ duration: '1m', target: 100 }, // Peak
{ duration: '1m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% under 500ms
http_req_failed: ['rate<0.01'], // <1% errors
},
};
export default function () {
const res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Accessibility Testing
Automated Checks
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('should have no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Manual Checklist
- Keyboard navigation works (Tab, Enter, Escape)
- Focus indicators visible
- Screen reader announces content correctly
- Color contrast meets WCAG AA (4.5:1)
- Form inputs have associated labels
- Images have alt text
- Error messages are announced
Common Anti-Patterns to Avoid
❌ Testing implementation details
// Bad: Testing internal state
expect(component.state.isLoading).toBe(true);
// Good: Testing observable behavior
expect(screen.getByRole('progressbar')).toBeInTheDocument();
❌ Flaky tests
// Bad: Time-dependent
expect(Date.now() - startTime).toBeLessThan(100);
// Good: Mock time
jest.useFakeTimers();
❌ Test interdependence
// Bad: Tests share state
let counter = 0;
it('test 1', () => { counter++; });
it('test 2', () => { expect(counter).toBe(1); }); // Depends on test 1
// Good: Isolated tests
beforeEach(() => { counter = 0; });
❌ Over-mocking
// Bad: Mock everything
jest.mock('../db');
jest.mock('../cache');
jest.mock('../utils');
// Test proves nothing
// Good: Mock boundaries only
jest.mock('../externalPaymentApi');
CI/CD Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info