- Published on
Software Testing Strategies Complete Guide 2025: Unit/Integration/E2E, TDD, Test Pyramid
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction
- 1. The Test Pyramid and Its Variants
- 2. Unit Testing
- 3. Integration Testing
- 4. E2E Testing (End-to-End Testing)
- 5. TDD (Test-Driven Development)
- 6. BDD (Behavior-Driven Development)
- 7. Mocking Strategies
- 8. Test Coverage
- 9. Component Testing (React)
- 10. Performance Testing
- 11. Contract Testing
- 12. CI Integration
- 13. Quiz
- 14. Testing Strategy Checklist
- 15. References
Introduction
In 2024, Kent Beck emphasized that "not writing tests is like driving without a seatbelt." As software grows increasingly complex, testing strategy is not optional -- it is essential. Netflix runs millions of tests daily across 10,000+ microservices, and Google continuously tests its entire codebase.
Yet many teams struggle to answer fundamental questions: "What tests should we write, how many, and how?" From the test pyramid to TDD/BDD, mocking strategies, and CI integration -- this guide systematically covers the testing strategies you need in practice in 2025.
1. The Test Pyramid and Its Variants
1.1 The Traditional Test Pyramid
The test pyramid, proposed by Mike Cohn in 2009, is the foundational framework for test strategy.
/\
/ \ E2E Tests (10%)
/ \ - Slow and expensive
/------\ - Full system verification
/ \ Integration Tests (20%)
/ \ - Service interactions
/------------\ Unit Tests (70%)
/ \ - Fast and cheap
/________________\ - Business logic verification
| Layer | Ratio | Speed | Maintenance Cost | Confidence |
|---|---|---|---|---|
| E2E | 10% | Slow (minutes) | High | Very High |
| Integration | 20% | Medium (seconds) | Medium | High |
| Unit | 70% | Fast (ms) | Low | Medium |
1.2 The Testing Trophy
Kent C. Dodds proposed the Testing Trophy, emphasizing integration tests from a frontend perspective.
___
| E | E2E (few)
|___|
/ \
/ Integ \ Integration (most)
/_________\
| Unit | Unit (moderate)
|_________|
| Static | Static Analysis (baseline)
|_________|
Core Philosophy: "The more your tests resemble the way your software is used, the more confidence they can give you."
1.3 The Testing Diamond
An approach adopted by Spotify for large-scale backend systems.
/\ E2E (few)
/ \
/ \
/------\ Integration (many)
\ / - DB, API, message queues
\ /
\ / Unit (moderate)
\/ - Pure business logic
1.4 Which Model Should You Choose?
| Project Type | Recommended Model | Reason |
|---|---|---|
| Frontend SPA | Testing Trophy | Component integration is key |
| Backend API | Testing Diamond | DB/external service integration matters |
| Microservices | Test Pyramid | Service isolation is key |
| Fullstack Monolith | Hybrid | Mix optimal strategies per layer |
2. Unit Testing
2.1 The AAA Pattern
The fundamental structure for all unit tests.
// Arrange-Act-Assert pattern
describe('calculateDiscount', () => {
it('applies 20% discount for VIP customers', () => {
// Arrange
const customer = createCustomer({ tier: 'VIP' });
const order = createOrder({ total: 10000 });
// Act
const result = calculateDiscount(customer, order);
// Assert
expect(result.discountRate).toBe(0.2);
expect(result.finalPrice).toBe(8000);
});
});
2.2 Jest vs Vitest Comparison
// ========== Jest Configuration ==========
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
],
};
// ========== Vitest Configuration ==========
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
resolve: {
alias: { '@': '/src' },
},
});
| Feature | Jest | Vitest |
|---|---|---|
| Speed | Moderate | Very fast (Vite-based) |
| Configuration | Separate config | Shares Vite config |
| ESM Support | Experimental | Native |
| Compatibility | Jest API | Jest-compatible API |
| HMR | None | Supported (watch mode) |
| Ecosystem | Very large | Growing rapidly |
2.3 Pytest (Python Backend)
# test_order_service.py
import pytest
from decimal import Decimal
from order_service import OrderService, Order, OrderItem
class TestOrderService:
"""Order service unit tests"""
@pytest.fixture
def order_service(self):
return OrderService()
@pytest.fixture
def sample_items(self):
return [
OrderItem(name="Laptop", price=Decimal("1200.00"), quantity=1),
OrderItem(name="Mouse", price=Decimal("50.00"), quantity=2),
]
def test_calculate_total(self, order_service, sample_items):
"""Correctly calculates the total amount"""
total = order_service.calculate_total(sample_items)
assert total == Decimal("1300.00")
def test_apply_bulk_discount(self, order_service, sample_items):
"""Applies 10% discount for orders over 1000"""
total = order_service.calculate_total(sample_items)
discounted = order_service.apply_discount(total, "BULK")
assert discounted == Decimal("1170.00")
@pytest.mark.parametrize("coupon_code,expected_discount", [
("WELCOME10", Decimal("0.10")),
("VIP20", Decimal("0.20")),
("SPECIAL30", Decimal("0.30")),
("INVALID", Decimal("0.00")),
])
def test_coupon_discount_rates(self, order_service, coupon_code, expected_discount):
"""Verifies discount rates by coupon code"""
rate = order_service.get_coupon_discount_rate(coupon_code)
assert rate == expected_discount
2.4 JUnit 5 (Java/Spring Boot)
// OrderServiceTest.java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Nested
@DisplayName("Create Order Tests")
class CreateOrderTest {
@Test
@DisplayName("Successfully creates a valid order")
void shouldCreateOrderSuccessfully() {
// Arrange
var request = new CreateOrderRequest("user-1", List.of(
new OrderItem("item-1", 2, BigDecimal.valueOf(10000))
));
when(orderRepository.save(any(Order.class)))
.thenReturn(Order.builder().id("order-1").build());
// Act
var result = orderService.createOrder(request);
// Assert
assertThat(result.getId()).isEqualTo("order-1");
verify(orderRepository).save(any(Order.class));
}
@Test
@DisplayName("Throws exception for empty item list")
void shouldThrowExceptionForEmptyItems() {
var request = new CreateOrderRequest("user-1", List.of());
assertThrows(InvalidOrderException.class,
() -> orderService.createOrder(request));
}
}
@ParameterizedTest
@CsvSource({
"10000, 0, 10000",
"10000, 10, 9000",
"50000, 20, 40000",
})
@DisplayName("Calculates final price based on discount rate")
void shouldCalculateFinalPrice(
int price, int discountPercent, int expected) {
var result = orderService.calculateFinalPrice(
BigDecimal.valueOf(price), discountPercent);
assertThat(result).isEqualByComparingTo(
BigDecimal.valueOf(expected));
}
}
3. Integration Testing
3.1 Database Integration Tests with Testcontainers
// order.integration.test.ts
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { PrismaClient } from '@prisma/client';
describe('OrderRepository Integration', () => {
let container;
let prisma: PrismaClient;
beforeAll(async () => {
// Start a real PostgreSQL container
container = await new PostgreSqlContainer('postgres:16')
.withDatabase('testdb')
.withUsername('test')
.withPassword('test')
.start();
// Connect Prisma
prisma = new PrismaClient({
datasources: {
db: { url: container.getConnectionUri() },
},
});
// Run migrations
await prisma.$executeRaw`CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255) NOT NULL,
total DECIMAL(10,2) NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
)`;
}, 60000);
afterAll(async () => {
await prisma.$disconnect();
await container.stop();
});
it('can create and retrieve an order', async () => {
const order = await prisma.order.create({
data: {
userId: 'user-123',
total: 500.00,
status: 'pending',
},
});
const found = await prisma.order.findUnique({
where: { id: order.id },
});
expect(found).toBeDefined();
expect(found?.userId).toBe('user-123');
expect(found?.total).toBe(500.00);
});
it('can list orders by user', async () => {
await prisma.order.createMany({
data: [
{ userId: 'user-A', total: 100.00, status: 'completed' },
{ userId: 'user-A', total: 200.00, status: 'pending' },
{ userId: 'user-B', total: 300.00, status: 'completed' },
],
});
const userAOrders = await prisma.order.findMany({
where: { userId: 'user-A' },
orderBy: { createdAt: 'desc' },
});
expect(userAOrders).toHaveLength(2);
});
});
3.2 Python Testcontainers
# test_user_repository.py
import pytest
from testcontainers.postgres import PostgresContainer
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models import User, Base
from app.repositories import UserRepository
@pytest.fixture(scope="module")
def postgres_container():
with PostgresContainer("postgres:16") as postgres:
yield postgres
@pytest.fixture(scope="module")
def db_session(postgres_container):
engine = create_engine(postgres_container.get_connection_url())
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
class TestUserRepository:
def test_create_and_find_user(self, db_session):
repo = UserRepository(db_session)
user = repo.create(name="John Doe", email="john@example.com")
assert user.id is not None
found = repo.find_by_id(user.id)
assert found.name == "John Doe"
assert found.email == "john@example.com"
def test_find_by_email(self, db_session):
repo = UserRepository(db_session)
repo.create(name="Jane Doe", email="jane@example.com")
found = repo.find_by_email("jane@example.com")
assert found is not None
assert found.name == "Jane Doe"
3.3 API Integration Testing
// app.integration.test.ts
import request from 'supertest';
import { app } from '../src/app';
import { prisma } from '../src/db';
describe('POST /api/orders', () => {
beforeEach(async () => {
await prisma.order.deleteMany();
});
it('creates an order with valid request (201)', async () => {
const response = await request(app)
.post('/api/orders')
.send({
userId: 'user-123',
items: [
{ productId: 'prod-1', quantity: 2, price: 15.00 },
],
})
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
status: 'pending',
total: 30.00,
});
});
it('returns 400 when required fields are missing', async () => {
const response = await request(app)
.post('/api/orders')
.send({ userId: 'user-123' })
.expect(400);
expect(response.body.error).toContain('items');
});
});
4. E2E Testing (End-to-End Testing)
4.1 Playwright vs Cypress Comparison
| Feature | Playwright | Cypress |
|---|---|---|
| Browser Support | Chromium, Firefox, WebKit | Chromium-based |
| Languages | JS/TS, Python, Java, .NET | JS/TS |
| Parallel Execution | Native support | Paid (Cypress Cloud) |
| Network Mocking | Built-in route API | cy.intercept |
| iframe Support | Full support | Limited |
| Tabs/Windows | Multi-tab support | Not supported |
| Speed | Fast | Moderate |
| Debugging | Trace Viewer, Codegen | Time Travel |
| Community | Growing rapidly | Mature ecosystem |
4.2 Playwright Practical Example
// tests/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await page.waitForURL('/dashboard');
});
test('complete flow from adding product to payment', async ({ page }) => {
// 1. Navigate to products
await page.goto('/products');
// 2. Add product to cart
await page.click('[data-testid="product-card-1"] button');
await expect(page.locator('[data-testid="cart-count"]'))
.toHaveText('1');
// 3. Go to cart
await page.click('[data-testid="cart-icon"]');
await expect(page).toHaveURL('/cart');
// 4. Change quantity
await page.fill('[data-testid="quantity-input"]', '3');
await expect(page.locator('[data-testid="subtotal"]'))
.toContainText('$45.00');
// 5. Proceed to checkout
await page.click('[data-testid="checkout-button"]');
// 6. Enter shipping info
await page.fill('#address', '123 Test Street, Suite 456');
await page.fill('#phone', '555-0123');
// 7. Complete payment
await page.click('[data-testid="pay-button"]');
await expect(page.locator('[data-testid="order-success"]'))
.toBeVisible();
await expect(page.locator('[data-testid="order-id"]'))
.toContainText('ORD-');
});
test('shows error message when out of stock', async ({ page }) => {
// Mock API: out of stock response
await page.route('**/api/checkout', (route) => {
route.fulfill({
status: 409,
body: JSON.stringify({
error: 'OUT_OF_STOCK',
message: 'Item is out of stock',
}),
});
});
await page.goto('/cart');
await page.click('[data-testid="checkout-button"]');
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('out of stock');
});
});
4.3 Page Object Model (POM) Pattern
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('[data-testid="email"]');
this.passwordInput = page.locator('[data-testid="password"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error"]');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// pages/DashboardPage.ts
export class DashboardPage {
readonly page: Page;
readonly welcomeMessage: Locator;
readonly orderList: Locator;
constructor(page: Page) {
this.page = page;
this.welcomeMessage = page.locator('[data-testid="welcome"]');
this.orderList = page.locator('[data-testid="order-list"]');
}
async expectWelcome(name: string) {
await expect(this.welcomeMessage).toContainText(name);
}
}
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('navigates to dashboard after successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboard = new DashboardPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password123');
await dashboard.expectWelcome('John Doe');
});
4.4 Visual Regression Testing
// visual.spec.ts
import { test, expect } from '@playwright/test';
test('main page visual regression', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('main-page.png', {
maxDiffPixelRatio: 0.01,
threshold: 0.2,
});
});
test('dark mode visual test', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="theme-toggle"]');
await expect(page).toHaveScreenshot('main-page-dark.png');
});
test('responsive design test', async ({ page }) => {
const viewports = [
{ width: 375, height: 812, name: 'mobile' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 1440, height: 900, name: 'desktop' },
];
for (const vp of viewports) {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('/');
await expect(page).toHaveScreenshot(
`main-page-${vp.name}.png`
);
}
});
5. TDD (Test-Driven Development)
5.1 The Red-Green-Refactor Cycle
┌──────────┐ ┌──────────┐ ┌──────────┐
│ RED │───▶│ GREEN │───▶│ REFACTOR │
│ Write │ │ Minimal │ │ Improve │
│ failing │ │ passing │ │ code, │
│ test │ │ code │ │ keep │
└──────────┘ └──────────┘ │ tests │
▲ └─────┬────┘
└────────────────────────────────┘
5.2 TDD Practical Example: Password Validator
// ========== Step 1: RED - Write a failing test ==========
describe('PasswordValidator', () => {
it('fails for passwords shorter than 8 characters', () => {
expect(validatePassword('Ab1!xyz')).toEqual({
valid: false,
errors: ['Password must be at least 8 characters'],
});
});
});
// ========== Step 2: GREEN - Minimal implementation ==========
export function validatePassword(password: string) {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
return { valid: errors.length === 0, errors };
}
// ========== Step 3: RED - Add next test ==========
it('fails when no uppercase letter is present', () => {
expect(validatePassword('abcd1234!')).toEqual({
valid: false,
errors: ['Must contain at least 1 uppercase letter'],
});
});
// ========== Step 4: GREEN - Extend implementation ==========
export function validatePassword(password: string) {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Must contain at least 1 uppercase letter');
}
return { valid: errors.length === 0, errors };
}
// ========== Step 5: Add more rules ==========
it('fails when no lowercase letter is present', () => {
expect(validatePassword('ABCD1234!')).toEqual({
valid: false,
errors: ['Must contain at least 1 lowercase letter'],
});
});
it('fails when no digit is present', () => {
expect(validatePassword('Abcdefgh!')).toEqual({
valid: false,
errors: ['Must contain at least 1 digit'],
});
});
it('fails when no special character is present', () => {
expect(validatePassword('Abcdefg1')).toEqual({
valid: false,
errors: ['Must contain at least 1 special character'],
});
});
it('passes when all conditions are met', () => {
expect(validatePassword('Abcdefg1!')).toEqual({
valid: true,
errors: [],
});
});
// ========== Step 6: REFACTOR - Clean up ==========
interface ValidationRule {
test: (password: string) => boolean;
message: string;
}
const rules: ValidationRule[] = [
{ test: (p) => p.length >= 8, message: 'Password must be at least 8 characters' },
{ test: (p) => /[A-Z]/.test(p), message: 'Must contain at least 1 uppercase letter' },
{ test: (p) => /[a-z]/.test(p), message: 'Must contain at least 1 lowercase letter' },
{ test: (p) => /\d/.test(p), message: 'Must contain at least 1 digit' },
{ test: (p) => /[!@#$%^&*]/.test(p), message: 'Must contain at least 1 special character' },
];
export function validatePassword(password: string) {
const errors = rules
.filter((rule) => !rule.test(password))
.map((rule) => rule.message);
return { valid: errors.length === 0, errors };
}
5.3 TDD Pros and Cons
Pros:
- Improved design (testable code = good design)
- Regression bug prevention
- Tests serve as documentation (tests are specs)
- Confidence when refactoring
Cons:
- Slower initial development speed
- Learning curve exists
- UI-related TDD is challenging
- Risk of over-testing
6. BDD (Behavior-Driven Development)
6.1 Cucumber/Gherkin Syntax
# features/order.feature
Feature: Online Ordering
As a user
I want to be able to place orders
Background:
Given a logged-in user exists
Scenario: Successful order
Given the cart contains 1 "Laptop"
And the cart contains 2 "Mouse"
When the user clicks the order button
Then the order is created successfully
And the order status is "Payment Pending"
And a confirmation email is sent
Scenario: Out of stock
Given the cart contains 1 "Limited Edition Keyboard"
And the "Limited Edition Keyboard" has 0 in stock
When the user clicks the order button
Then "Out of stock" message is displayed
Scenario Outline: Shipping cost calculation
Given the order total is <total>
When shipping cost is calculated
Then the shipping cost is <shipping>
Examples:
| total | shipping |
| 30.00 | 5.00 |
| 50.00 | 0.00 |
| 100.00 | 0.00 |
7. Mocking Strategies
7.1 Mock vs Stub vs Spy vs Fake
┌──────────────────────────────────────────────────────────────┐
│ Test Doubles Classification │
├──────────┬───────────────────────────────────────────────────┤
│ Dummy │ Fills parameters. Never actually used │
│ Stub │ Returns predefined values. Fixed responses │
│ Spy │ Real implementation + call recording │
│ Mock │ Pre-set expected calls. Verifies behavior │
│ Fake │ Simple working implementation (in-memory DB, etc.) │
└──────────┴───────────────────────────────────────────────────┘
7.2 Jest/Vitest Mocking Patterns
// ========== Function Mocking ==========
import { vi, describe, it, expect } from 'vitest';
import { sendEmail } from './emailService';
import { createOrder } from './orderService';
// Mock entire module
vi.mock('./emailService', () => ({
sendEmail: vi.fn().mockResolvedValue({ sent: true }),
}));
describe('createOrder', () => {
it('sends email after creating order', async () => {
const order = await createOrder({ userId: 'user-1', items: [] });
// Verify call (Spy pattern)
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: expect.any(String),
subject: expect.stringContaining('order'),
})
);
});
});
// ========== API Mocking (MSW) ==========
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/products', () => {
return HttpResponse.json([
{ id: '1', name: 'Laptop', price: 1200.00 },
{ id: '2', name: 'Mouse', price: 50.00 },
]);
}),
http.post('/api/orders', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: 'order-123', ...body, status: 'pending' },
{ status: 201 }
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
7.3 When Should You Mock?
| Mock This | Do Not Mock This |
|---|---|
| External API calls | Pure functions/business logic |
| Database (unit tests) | Value Objects |
| File system | The test subject itself |
| Time/Date functions | Simple helper functions |
| Email/SMS sending | Nearby dependencies |
| Payment gateways | Data transformation logic |
The Golden Rule of Mocking: "Don't mock what you don't own."
8. Test Coverage
8.1 Coverage Types
┌─────────────────────────────────────────────────────┐
│ Coverage Types │
├──────────────┬──────────────────────────────────────┤
│ Line │ Percentage of executed lines │
│ Branch │ Percentage of branches covered │
│ Function │ Percentage of functions called │
│ Statement │ Percentage of statements executed │
└──────────────┴──────────────────────────────────────┘
8.2 Mutation Testing
Mutation testing compensates for the limitations of traditional coverage.
// Original code
function isAdult(age: number): boolean {
return age >= 18;
}
// Mutation 1: Boundary change
function isAdult(age: number): boolean {
return age > 18; // changed >= to >
}
// Mutation 2: Operator change
function isAdult(age: number): boolean {
return age <= 18; // changed >= to <=
}
// If tests don't catch these mutations = weak tests
# Stryker (JavaScript/TypeScript)
npx stryker run
# Example output
# Mutation score: 85%
# Survived mutants: 12
# Killed mutants: 68
# Timeouts: 3
8.3 Reasonable Coverage Targets
| Project Type | Line | Branch | Notes |
|---|---|---|---|
| Core Business Logic | 90%+ | 85%+ | Financial, payment-related |
| General Backend API | 80%+ | 75%+ | Including CRUD |
| Frontend UI | 70%+ | 65%+ | Excluding styles |
| Utility Libraries | 95%+ | 90%+ | Reusable code |
Avoid the Coverage Trap: 100% coverage does not guarantee zero bugs. Meaningful assertions matter more.
9. Component Testing (React)
9.1 React Testing Library
// components/SearchBar.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SearchBar } from './SearchBar';
describe('SearchBar', () => {
it('displays results after entering a search term', async () => {
const user = userEvent.setup();
const onSearch = vi.fn().mockResolvedValue([
{ id: 1, title: 'React Basics' },
{ id: 2, title: 'Advanced React' },
]);
render(<SearchBar onSearch={onSearch} />);
const input = screen.getByPlaceholderText('Enter search term');
await user.type(input, 'React');
await user.click(screen.getByRole('button', { name: 'Search' }));
await waitFor(() => {
expect(screen.getByText('React Basics')).toBeInTheDocument();
expect(screen.getByText('Advanced React')).toBeInTheDocument();
});
expect(onSearch).toHaveBeenCalledWith('React');
});
it('shows error for empty search', async () => {
const user = userEvent.setup();
render(<SearchBar onSearch={vi.fn()} />);
await user.click(screen.getByRole('button', { name: 'Search' }));
expect(screen.getByText('Please enter a search term'))
.toBeInTheDocument();
});
});
10. Performance Testing
10.1 k6 Load Testing
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const orderDuration = new Trend('order_duration');
export const options = {
stages: [
{ duration: '2m', target: 50 }, // Warm up
{ duration: '5m', target: 200 }, // Ramp up
{ duration: '3m', target: 500 }, // Peak load
{ duration: '2m', target: 0 }, // Cool down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95th percentile under 500ms
errors: ['rate<0.01'], // Error rate under 1%
order_duration: ['p(99)<1000'], // 99th percentile under 1s
},
};
export default function () {
// List products
const productsRes = http.get('http://localhost:3000/api/products');
check(productsRes, {
'products 200': (r) => r.status === 200,
'response under 200ms': (r) => r.timings.duration < 200,
});
// Create order
const orderStart = Date.now();
const orderRes = http.post(
'http://localhost:3000/api/orders',
JSON.stringify({
userId: `user-${__VU}`,
items: [{ productId: 'prod-1', quantity: 1 }],
}),
{ headers: { 'Content-Type': 'application/json' } }
);
orderDuration.add(Date.now() - orderStart);
errorRate.add(orderRes.status !== 201);
check(orderRes, {
'order created 201': (r) => r.status === 201,
});
sleep(1);
}
11. Contract Testing
11.1 Consumer-Driven Contracts with Pact
// consumer.pact.test.ts (Frontend - Consumer)
import { PactV3 } from '@pact-foundation/pact';
const provider = new PactV3({
consumer: 'frontend-app',
provider: 'order-api',
});
describe('Order API Contract', () => {
it('fetches order list', async () => {
provider
.given('orders exist')
.uponReceiving('a request for order list')
.withRequest({
method: 'GET',
path: '/api/orders',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: [
{
id: 'order-1',
status: 'pending',
total: 500.00,
createdAt: '2025-01-15T10:00:00Z',
},
],
});
await provider.executeTest(async (mockService) => {
const response = await fetch(
`${mockService.url}/api/orders`,
{ headers: { Accept: 'application/json' } }
);
const orders = await response.json();
expect(orders).toHaveLength(1);
expect(orders[0].id).toBe('order-1');
});
});
});
12. CI Integration
12.1 GitHub Actions Test Pipeline
# .github/workflows/test.yml
name: Test Pipeline
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-node-${{ matrix.node-version }}
path: coverage/
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E Tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
12.2 Parallel Test Execution
# Playwright sharding example
e2e-tests:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/4
13. Quiz
Q1: What is the correct ratio in the test pyramid?
A) E2E 50%, Integration 30%, Unit 20% B) Unit 70%, Integration 20%, E2E 10% C) Unit 33%, Integration 33%, E2E 33% D) E2E 70%, Integration 20%, Unit 10%
Answer: B
The test pyramid has the most unit tests and the fewest E2E tests. Unit tests are fast with low maintenance cost, while E2E tests are slow and expensive.
Q2: What is the correct order of TDD's Red-Green-Refactor cycle?
A) Write code - Write test - Refactor B) Write failing test - Minimal implementation - Refactor C) Refactor - Write test - Write code D) Minimal implementation - Write test - Refactor
Answer: B
TDD starts with writing a failing test (Red), then writing minimal code to pass the test (Green), then improving the code while keeping tests passing (Refactor).
Q3: What is the main difference between Mock and Stub?
A) Mocks are faster, Stubs are slower B) Mocks verify behavior (calls), Stubs verify state (return values) C) Mocks are for frontend, Stubs are for backend D) No difference
Answer: B
Stubs focus on returning predefined values (state verification), while Mocks verify whether specific methods were called with specific arguments (behavior verification).
Q4: What is the biggest difference between Playwright and Cypress?
A) Playwright is paid, Cypress is free B) Playwright supports multi-browser/tab, Cypress supports single tab only C) Cypress is faster D) Playwright only supports JavaScript
Answer: B
Playwright supports Chromium, Firefox, and WebKit with multi-tab/window testing. Cypress runs in Chromium-based browsers with a single tab. Playwright supports JS/TS, Python, Java, and .NET.
Q5: What is the purpose of Mutation Testing?
A) Measures code performance B) Evaluates the quality (strength) of test code C) Finds security vulnerabilities D) Checks code style
Answer: B
Mutation testing introduces small changes (operator changes, condition inversions, etc.) to source code and checks whether existing tests detect these mutations. Undetected mutations indicate weak tests.
14. Testing Strategy Checklist
Project Testing Strategy Checklist:
[ ] 1. Choose testing model (Pyramid/Trophy/Diamond)
[ ] 2. Select testing framework (Jest/Vitest/Pytest/JUnit)
[ ] 3. Unit tests - Cover business logic
[ ] 4. Integration tests - Verify DB/API integration
[ ] 5. E2E tests - Critical user flows
[ ] 6. Establish mocking strategy
[ ] 7. CI pipeline integration
[ ] 8. Set coverage targets
[ ] 9. Monitor flaky tests
[ ] 10. Performance testing (k6/Artillery)
15. References
- Testing JavaScript - Kent C. Dodds: testingjavascript.com
- Playwright Documentation: playwright.dev
- Vitest Documentation: vitest.dev
- Pytest Documentation: docs.pytest.org
- JUnit 5 User Guide: junit.org/junit5
- Martin Fowler - Test Pyramid: martinfowler.com/bliki/TestPyramid.html
- Testcontainers: testcontainers.com
- MSW (Mock Service Worker): mswjs.io
- k6 Load Testing: k6.io
- Pact Contract Testing: pact.io
- Stryker Mutator: stryker-mutator.io
- Google Testing Blog: testing.googleblog.com
- Cypress Documentation: docs.cypress.io
- React Testing Library: testing-library.com/react