Skip to content
Published on

Software Testing Strategies Complete Guide 2025: Unit/Integration/E2E, TDD, Test Pyramid

Authors

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
LayerRatioSpeedMaintenance CostConfidence
E2E10%Slow (minutes)HighVery High
Integration20%Medium (seconds)MediumHigh
Unit70%Fast (ms)LowMedium

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 TypeRecommended ModelReason
Frontend SPATesting TrophyComponent integration is key
Backend APITesting DiamondDB/external service integration matters
MicroservicesTest PyramidService isolation is key
Fullstack MonolithHybridMix 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' },
  },
});
FeatureJestVitest
SpeedModerateVery fast (Vite-based)
ConfigurationSeparate configShares Vite config
ESM SupportExperimentalNative
CompatibilityJest APIJest-compatible API
HMRNoneSupported (watch mode)
EcosystemVery largeGrowing 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

FeaturePlaywrightCypress
Browser SupportChromium, Firefox, WebKitChromium-based
LanguagesJS/TS, Python, Java, .NETJS/TS
Parallel ExecutionNative supportPaid (Cypress Cloud)
Network MockingBuilt-in route APIcy.intercept
iframe SupportFull supportLimited
Tabs/WindowsMulti-tab supportNot supported
SpeedFastModerate
DebuggingTrace Viewer, CodegenTime Travel
CommunityGrowing rapidlyMature 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   │───▶│ REFACTORWrite   │    │  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├──────────┬───────────────────────────────────────────────────┤
DummyFills parameters. Never actually used              │
StubReturns predefined values. Fixed responses         │
SpyReal implementation + call recording               │
MockPre-set expected calls. Verifies behavior          │
FakeSimple 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 ThisDo Not Mock This
External API callsPure functions/business logic
Database (unit tests)Value Objects
File systemThe test subject itself
Time/Date functionsSimple helper functions
Email/SMS sendingNearby dependencies
Payment gatewaysData transformation logic

The Golden Rule of Mocking: "Don't mock what you don't own."


8. Test Coverage

8.1 Coverage Types

┌─────────────────────────────────────────────────────┐
Coverage Types├──────────────┬──────────────────────────────────────┤
LinePercentage of executed lines           │
BranchPercentage of branches covered         │
FunctionPercentage of functions called          │
StatementPercentage 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 TypeLineBranchNotes
Core Business Logic90%+85%+Financial, payment-related
General Backend API80%+75%+Including CRUD
Frontend UI70%+65%+Excluding styles
Utility Libraries95%+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

  1. Testing JavaScript - Kent C. Dodds: testingjavascript.com
  2. Playwright Documentation: playwright.dev
  3. Vitest Documentation: vitest.dev
  4. Pytest Documentation: docs.pytest.org
  5. JUnit 5 User Guide: junit.org/junit5
  6. Martin Fowler - Test Pyramid: martinfowler.com/bliki/TestPyramid.html
  7. Testcontainers: testcontainers.com
  8. MSW (Mock Service Worker): mswjs.io
  9. k6 Load Testing: k6.io
  10. Pact Contract Testing: pact.io
  11. Stryker Mutator: stryker-mutator.io
  12. Google Testing Blog: testing.googleblog.com
  13. Cypress Documentation: docs.cypress.io
  14. React Testing Library: testing-library.com/react