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
- Navigate to [page/screen]
- Enter [specific data]
- Click [button/action]
- 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