
testing-strategies
Modern testing strategies covering TDD/BDD, shift-left/shift-right, test automation, and quality ass
by mdaashir|Open Source
Testing Strategies
A comprehensive skill covering modern testing methodologies, automation strategies, and quality assurance practices based on 2025-2026 industry standards.
Table of Contents
- Testing Pyramid & Trophy
- Test-Driven Development (TDD)
- Behavior-Driven Development (BDD)
- Shift-Left Testing
- Shift-Right Testing
- Test Automation
- Quality Metrics
- Testing Tools
Testing Pyramid & Trophy
Testing Pyramid (Traditional)
/\
/E2E\ ← Few, slow, expensive
/------\
/ API \ ← Medium quantity, faster
/----------\
/ Unit \ ← Many, fast, cheap
/--------------\
Testing Trophy (Modern, Recommended)
/\
/E2E\ ← Few critical paths
/------\
/Integr. \ ← Most tests here (70%)
/----------\
/ Unit \ ← Important utilities (20%)
/--------------\
Static Analysis ← TypeScript, ESLint, etc.
Why Testing Trophy?
- Integration tests catch more real bugs
- Provide better confidence
- Good balance of speed and reliability
- Reflect actual user workflows
Test-Driven Development (TDD)
The Red-Green-Refactor Cycle
1. 🔴 RED: Write a failing test
↓
2. 🟢 GREEN: Make it pass (minimal code)
↓
3. 🔵 REFACTOR: Improve the code
↓
Repeat
TDD Example: User Registration
// 1. RED: Write failing test first
describe('UserRegistration', () => {
it('should create a new user with hashed password', async () => {
const service = new UserRegistrationService();
const userData = {
email: 'user@example.com',
password: 'SecurePass123!',
name: 'John Doe',
};
const user = await service.register(userData);
expect(user.email).toBe(userData.email);
expect(user.name).toBe(userData.name);
expect(user.password).not.toBe(userData.password); // Should be hashed
expect(user.password).toHaveLength(60); // bcrypt hash length
});
it('should reject duplicate email', async () => {
const service = new UserRegistrationService();
const userData = {
email: 'existing@example.com',
password: 'SecurePass123!',
name: 'Jane Doe',
};
await service.register(userData);
await expect(service.register(userData)).rejects.toThrow('Email already exists');
});
});
// 2. GREEN: Implement minimal code to pass
class UserRegistrationService {
constructor(private userRepo: UserRepository) {}
async register(userData: RegisterUserDTO): Promise<User> {
// Check for existing email
const existing = await this.userRepo.findByEmail(userData.email);
if (existing) {
throw new Error('Email already exists');
}
// Hash password
const hashedPassword = await bcrypt.hash(userData.password, 10);
// Create user
const user = new User({
email: userData.email,
name: userData.name,
password: hashedPassword,
});
await this.userRepo.save(user);
return user;
}
}
// 3. REFACTOR: Extract hash logic, add validation, etc.
TDD Benefits
- Better Design: Forces you to think about interfaces first
- Regression Safety: Tests act as safety net
- Documentation: Tests show how code should be used
- Confidence: Refactor without fear
Behavior-Driven Development (BDD)
Given-When-Then Format
Feature: User Authentication
Scenario: Successful login with valid credentials
Given a registered user with email "user@example.com"
And the user's password is "SecurePass123!"
When the user attempts to login
Then the login should succeed
And an authentication token should be generated
Scenario: Failed login with invalid password
Given a registered user with email "user@example.com"
And the user's password is "SecurePass123!"
When the user attempts to login with password "WrongPass"
Then the login should fail
And an error message "Invalid credentials" should be shown
BDD Implementation (Cucumber/Jest)
import { Given, When, Then, Before } from '@cucumber/cucumber';
import { expect } from 'chai';
let user: User;
let authResult: AuthResult;
let authService: AuthService;
Before(async () => {
authService = new AuthService();
await setupTestDatabase();
});
Given('a registered user with email {string}', async (email: string) => {
user = await createTestUser({ email, password: 'SecurePass123!' });
});
When('the user attempts to login', async () => {
authResult = await authService.login({
email: user.email,
password: 'SecurePass123!',
});
});
When('the user attempts to login with password {string}', async (password: string) => {
try {
authResult = await authService.login({
email: user.email,
password,
});
} catch (error) {
authResult = { success: false, error: error.message };
}
});
Then('the login should succeed', () => {
expect(authResult.success).to.be.true;
});
Then('an authentication token should be generated', () => {
expect(authResult.token).to.be.a('string');
expect(authResult.token).to.have.lengthOf.at.least(20);
});
Then('the login should fail', () => {
expect(authResult.success).to.be.false;
});
Then('an error message {string} should be shown', (message: string) => {
expect(authResult.error).to.equal(message);
});
Shift-Left Testing
Shift-Left: Find bugs early in development cycle
Static Analysis (Leftmost)
// TypeScript catches errors before runtime
interface User {
id: string;
email: string;
age: number;
}
function getUser(id: string): User {
// TypeScript enforces return type
return {
id,
email: 'user@example.com',
age: 25,
};
}
// ✅ Compile-time error
const user = getUser(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
Pre-Commit Hooks
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.ts": ["eslint --fix", "prettier --write", "jest --bail --findRelatedTests"]
}
}
IDE Integration
- Real-time linting: ESLint, TSLint
- Type checking: TypeScript language server
- Test running: Jest, Vitest integrations
- Code coverage: Inline coverage indicators
Code Review Automation
# .github/workflows/pr-checks.yml
name: PR Checks
on: [pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run linter
run: npm run lint
- name: Type check
run: npm run type-check
- name: Run unit tests
run: npm test -- --coverage
- name: Check coverage threshold
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
- name: Security audit
run: npm audit --audit-level=moderate
Shift-Right Testing
Shift-Right: Test in production-like environments
Canary Releases
class CanaryDeployment {
async deploy(newVersion: string, canaryPercentage: number) {
// Deploy to small subset of users
await this.deployToCanary(newVersion, canaryPercentage);
// Monitor metrics
const metrics = await this.monitorCanary(duration: 30 * 60 * 1000); // 30 min
// Validate success criteria
if (this.meetsSuccessCriteria(metrics)) {
await this.promoteToProduction(newVersion);
} else {
await this.rollback(newVersion);
throw new Error('Canary deployment failed validation');
}
}
private meetsSuccessCriteria(metrics: Metrics): boolean {
return (
metrics.errorRate < 0.01 && // < 1% error rate
metrics.p95Latency < 500 && // < 500ms P95 latency
metrics.successRate > 0.99 // > 99% success rate
);
}
}
Feature Flags
class FeatureFlagService {
async isEnabled(feature: string, user: User): Promise<boolean> {
const flag = await this.getFeatureFlag(feature);
if (!flag.enabled) return false;
// Gradual rollout
if (flag.rolloutPercentage < 100) {
const userHash = this.hashUser(user.id);
if (userHash % 100 >= flag.rolloutPercentage) {
return false;
}
}
// User targeting
if (flag.targetUsers && !flag.targetUsers.includes(user.id)) {
return false;
}
return true;
}
}
// Usage
if (await featureFlags.isEnabled('new-checkout-flow', user)) {
return await newCheckoutFlow(order);
} else {
return await legacyCheckoutFlow(order);
}
Synthetic Monitoring
// Monitor critical user journeys in production
class SyntheticMonitor {
async runUserJourney(): Promise<void> {
const startTime = Date.now();
try {
// Simulate user login
const loginResponse = await this.login({
email: 'synthetic-user@example.com',
password: process.env.SYNTHETIC_PASSWORD,
});
// Simulate browsing products
const products = await this.getProducts();
expect(products).to.have.length.greaterThan(0);
// Simulate adding to cart
await this.addToCart(products[0].id);
// Record success
await this.recordMetric('user-journey', {
success: true,
duration: Date.now() - startTime,
});
} catch (error) {
// Alert on failure
await this.alert('User journey failed', error);
await this.recordMetric('user-journey', {
success: false,
duration: Date.now() - startTime,
error: error.message,
});
}
}
}
// Run every 5 minutes
setInterval(() => new SyntheticMonitor().runUserJourney(), 5 * 60 * 1000);
Chaos Engineering
class ChaosMonkey {
async introduceLatency(service: string, latencyMs: number, percentage: number) {
// Add latency to X% of requests
return (req, res, next) => {
if (Math.random() < percentage / 100) {
setTimeout(() => next(), latencyMs);
} else {
next();
}
};
}
async killRandomInstance(serviceName: string) {
const instances = await this.getServiceInstances(serviceName);
const randomInstance = instances[Math.floor(Math.random() * instances.length)];
console.log(`Killing instance: ${randomInstance.id}`);
await this.terminateInstance(randomInstance.id);
}
async corruptData(percentage: number) {
// Introduce data corruption to test error handling
}
}
// Schedule chaos experiments
if (process.env.NODE_ENV === 'staging') {
const chaosMonkey = new ChaosMonkey();
// Every hour, kill a random instance
setInterval(
() => {
chaosMonkey.killRandomInstance('api-service');
},
60 * 60 * 1000
);
}
Test Automation
Unit Testing Best Practices
// ✅ Good: Focused, single assertion
describe('PasswordValidator', () => {
it('should require minimum 8 characters', () => {
const validator = new PasswordValidator();
expect(validator.validate('short')).toBe(false);
});
it('should require at least one uppercase letter', () => {
const validator = new PasswordValidator();
expect(validator.validate('lowercase123')).toBe(false);
});
it('should accept valid password', () => {
const validator = new PasswordValidator();
expect(validator.validate('ValidPass123!')).toBe(true);
});
});
// ❌ Bad: Multiple assertions, unclear purpose
it('should validate password', () => {
const validator = new PasswordValidator();
expect(validator.validate('short')).toBe(false);
expect(validator.validate('lowercase123')).toBe(false);
expect(validator.validate('ValidPass123!')).toBe(true);
});
Integration Testing
describe('User API', () => {
let app: Express;
let db: Database;
beforeAll(async () => {
db = await setupTestDatabase();
app = createApp(db);
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
});
it('POST /users should create new user', async () => {
const response = await request(app)
.post('/users')
.send({
email: 'test@example.com',
name: 'Test User',
password: 'SecurePass123!',
})
.expect(201);
expect(response.body).toMatchObject({
email: 'test@example.com',
name: 'Test User',
});
expect(response.body.password).toBeUndefined(); // Should not return password
expect(response.body.id).toBeDefined();
// Verify in database
const user = await db.users.findById(response.body.id);
expect(user).toBeDefined();
expect(user.password).not.toBe('SecurePass123!'); // Should be hashed
});
});
E2E Testing (Playwright)
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test('should complete purchase successfully', async ({ page }) => {
// 1. Login
await page.goto('/login');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
// 2. Browse products
await page.goto('/products');
await page.click('text=Add to Cart', { first: true });
// 3. View cart
await page.click('[aria-label="Cart"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
// 4. Checkout
await page.click('text=Proceed to Checkout');
// 5. Fill shipping info
await page.fill('[name="address"]', '123 Main St');
await page.fill('[name="city"]', 'San Francisco');
await page.fill('[name="zip"]', '94105');
// 6. Fill payment info (using test card)
await page.fill('[name="cardNumber"]', '4242424242424242');
await page.fill('[name="expiry"]', '12/25');
await page.fill('[name="cvc"]', '123');
// 7. Submit
await page.click('button:has-text("Place Order")');
// 8. Verify success
await expect(page.locator('text=Order Confirmed')).toBeVisible();
await expect(page.locator('.order-number')).toHaveText(/ORD-\d+/);
});
});
Visual Regression Testing
import { test, expect } from '@playwright/test';
test('Homepage visual regression', async ({ page }) => {
await page.goto('/');
// Take screenshot and compare to baseline
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // Allow minor differences
});
});
test('Responsive design', async ({ page }) => {
// Desktop
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/');
await expect(page).toHaveScreenshot('homepage-desktop.png');
// Tablet
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
Quality Metrics
Code Coverage
# Run tests with coverage
npm test -- --coverage
# Coverage thresholds (jest.config.js)
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
Mutation Testing
// Original code
function isEven(n: number): boolean {
return n % 2 === 0;
}
// Mutated code (tool generates this)
function isEven(n: number): boolean {
return n % 2 === 1; // Mutation: === changed to !==
}
// If tests still pass, mutation survived = weak tests!
Test Quality Metrics
- Test Coverage: % of code executed by tests
- Mutation Score: % of mutations killed by tests
- Test Execution Time: How fast tests run
- Flakiness Rate: % of tests that fail intermittently
- Bug Escape Rate: Bugs found in production vs. caught by tests
Testing Tools
Unit & Integration Testing
- Jest: JavaScript/TypeScript testing framework
- Vitest: Next-gen Vite-native testing
- Mocha + Chai: Flexible testing combo
- pytest: Python testing framework
E2E Testing
- Playwright: Modern cross-browser testing
- Cypress: Developer-friendly E2E testing
- Selenium: Legacy but widely supported
API Testing
- Supertest: HTTP assertion library
- Postman: API testing platform
- REST Client: VS Code extension
Performance Testing
- k6: Load testing tool
- Artillery: Modern load testing
- JMeter: Traditional load testing
Accessibility Testing
- axe-core: Accessibility engine
- Pa11y: Accessibility testing tool
- Lighthouse: Automated auditing
Summary
Modern testing strategies emphasize:
- Shift-Left: Catch bugs early with static analysis and TDD
- Automation: 80%+ test automation coverage
- Integration Focus: Most tests at integration level
- Shift-Right: Production monitoring and canary releases
- Continuous Testing: Tests run on every commit
- Quality Metrics: Track and improve test effectiveness
Choose the right testing level:
- Unit: Pure functions, utilities, algorithms
- Integration: API endpoints, database operations
- E2E: Critical user journeys
- Production: Synthetic monitoring, chaos engineering