jongkwan.dev
Development · Essay №058

Test Automation

A practical guide to unit/integration/E2E tests, the test pyramid, contract testing, and chaos engineering

Jongkwan Lee2026년 3월 5일11 min read
Contents

Test automation is the work of deciding how to distribute unit, integration, and E2E tests, and how far to push verification with contract, mutation, and chaos testing.

Overview

Test automation means writing unit, integration, and E2E tests as code and running them repeatedly. Organizations that rely on manual testing deploy slowly, are exposed to regression bugs, and accumulate technical debt over time.

As AI-driven test generation spreads, the shape of automation is changing.

Test levels

1. Unit tests

The smallest level of test, verifying an individual function, method, or module in isolation.

Characteristics:

  • The fastest feedback loop (millisecond execution)
  • External dependencies replaced by mocks or stubs
  • Easy to reach high coverage
  • Acts as a safety net during refactoring

Example (Jest):

javascript
describe('calculateDiscount', () => {
  it('computes a 10% discount correctly', () => {
    const result = calculateDiscount(10000, 0.1);
    expect(result).toBe(9000);
  });
 
  it('returns the original price when the discount rate is 0', () => {
    const result = calculateDiscount(10000, 0);
    expect(result).toBe(10000);
  });
 
  it('throws on a negative discount rate', () => {
    expect(() => calculateDiscount(10000, -0.1)).toThrow();
  });
});

TDD (Test-Driven Development):

  1. RED: write a failing test first
  2. GREEN: write the minimum code that makes it pass
  3. REFACTOR: improve the code while keeping the tests passing

2. Integration tests

Verify how several modules behave when they work together. This covers integration with real infrastructure such as databases, external APIs, and message queues.

What they cover:

  • Correctness of database CRUD operations
  • External API calls and response handling
  • Message queue integration (Kafka, RabbitMQ)
  • Cache (Redis) behavior
  • Transaction boundaries

Using Testcontainers:

Testcontainers is a library that spins up real databases, message brokers, and similar dependencies as temporary Docker containers during tests. Using real infrastructure instead of mocks raises confidence in the results.

java
@Testcontainers
class UserRepositoryTest {
    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");
 
    @Test
    void saves_and_retrieves_a_user_correctly() {
        UserRepository repo = new UserRepository(postgres.getJdbcUrl());
        User user = new User("test@example.com", "John Doe");
 
        repo.save(user);
        User found = repo.findByEmail("test@example.com");
 
        assertThat(found.getName()).isEqualTo("John Doe");
    }
}

3. E2E tests

Simulate a full user scenario to verify the system end to end.

Characteristics:

  • Slowest and most expensive, but closest to the real user experience
  • Run through browser automation (Playwright, Cypress) or at the API level
  • Kept to a minimum, focused on core business flows

Playwright example:

typescript
test('a user can search for a product and add it to the cart', async ({ page }) => {
  await page.goto('https://shop.example.com');
  await page.fill('[data-testid="search-input"]', 'laptop');
  await page.click('[data-testid="search-button"]');
 
  await expect(page.locator('.product-card')).toHaveCount.greaterThan(0);
 
  await page.click('.product-card:first-child [data-testid="add-to-cart"]');
  await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
});

Test strategy models

The test pyramid

The traditional model proposed by Martin Fowler: the number of tests decreases as you move upward.

text
        /  E2E  \          ← few (slow, expensive)
       /Integration\       ← moderate
      / Unit Tests  \      ← many (fast, cheap)
     ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾

The testing trophy

The modern model proposed by Kent C. Dodds, which recommends writing the most integration tests.

text
        /   E2E   \
       / Integration\      ← the most (best return on cost)
      /   Unit Tests  \
     /  Static Analysis \  ← TypeScript, ESLint, and so on
     ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
ModelCore claimWhere it fits
Test pyramidMost tests are unit testsLibraries with a lot of pure logic
Testing trophyMost tests are integration testsWeb applications, API servers

Advanced techniques

Contract testing

Verifies the API contract between microservices. It checks that neither the provider nor the consumer violates the interface both sides agreed on.

The Pact framework:

  • The consumer defines the request/response pairs it expects in a "pact file"
  • The provider is verified automatically against that pact file
  • Contracts are managed centrally through a Pact Broker

Mutation testing

Injects deliberate defects (mutants) into the source code and checks whether the existing tests catch them, which measures the quality of the test suite.

Mutation types:

  • Conditional: >>=, ==!=
  • Arithmetic: +-, */
  • Return value: return truereturn false
  • Removal: delete a statement

Tools: Stryker (JavaScript/TypeScript), PITest (Java), mutmut (Python)

Mutation score:

text
mutation score = (killed mutants / total mutants) * 100%

A score of 80% or higher is generally taken as a healthy test suite.

Property-based testing

Instead of specific inputs, you define a property, and the framework generates thousands of random inputs to check that the property always holds.

typescript
// example using the fast-check library
import fc from 'fast-check';
 
test('a sort function always returns a sorted array', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (arr) => {
      const sorted = mySort(arr);
      for (let i = 1; i < sorted.length; i++) {
        expect(sorted[i]).toBeGreaterThanOrEqual(sorted[i - 1]);
      }
    })
  );
});

Benefit: automatically finds edge cases the developer never thought of

Snapshot testing

Stores rendering output or API responses as snapshot files and reports the diff when something changes later.

Good candidates:

  • Rendered output of React components
  • JSON structure of API responses
  • Generated configuration files

Caveat: snapshots that are too large or change too often drive up maintenance cost. Capture only the structure that matters.

Chaos engineering

The idea

A methodology that injects failures deliberately in production or a production-like environment to verify system resilience. It started with Netflix's Chaos Monkey and has since expanded into the tools and frameworks listed in the table below.

Principles of chaos engineering

  1. Define a steady-state hypothesis: state what normal system behavior looks like
  2. Introduce experimental variables: server failures, network latency, disk exhaustion, and so on
  3. Compare a control group with an experiment group: measure impact in A/B form
  4. Minimize the blast radius: expand gradually

Main tools

ToolCharacteristicsEnvironment
Chaos MonkeyNetflix OSS, terminates random instancesAWS
LitmusKubernetes-native chaosKubernetes
Chaos ToolkitDeclarative chaos experiment definitionsGeneral purpose
GremlinEnterprise-grade SaaSGeneral purpose
SteadybitReliability testing built around team collaborationKubernetes/cloud

AI-driven chaos testing

Uber: DragonCrawl + uHavoc

Uber combined an LLM-based mobile testing platform (DragonCrawl) with a service-level fault injection system (uHavoc). Since Q1 2024 it has run more than 180,000 automated chaos tests across 47 core flows in the Rider, Driver, and Eats apps. That volume corresponds to roughly 39,000 hours of manual testing (source: arXiv:2602.06223).

uHavoc injects three kinds of failure at the Remote Procedure Call (RPC) layer.

  • Abort: force a 4xx/5xx error
  • Timeout: fail after a delayed response
  • Latency: degrade response time

Injection targets are limited to non-critical service tiers (the lower-importance T2 through T5 grades), and test tenancy isolation keeps production traffic untouched. A DragonCrawl run carries its failure configuration in a header for uHavoc to apply, so one flow definition can drive many failure scenarios. There is no need to write a separate test case for every combination of city, flow, and failure type.

DragonCrawl survives failure conditions because it reads the screen by meaning rather than by selector. The question "can a trip be booked from this screen?" can still be answered when the fare shows "Calculating..." instead of a concrete amount. Tests do not break when injected failures leave UI elements empty or delayed, and the pass rate stayed above 99% even with T2-tier failure injection.

Twenty-three resilience risks were found. Twelve of them were serious defects that blocked trip requests or food orders outright, and two were app crashes only reachable through mobile testing. Roughly 70% of the issues found were architectural dependency violations in which a non-critical service failure propagated into a core flow. Automated root-cause attribution reached 88% precision@5, cutting debugging time for engineers unfamiliar with the system from about three hours to under five minutes.

Red Hat and IBM Research: the Krkn chaos-recommender

Teams at Red Hat and IBM Research added a chaos-recommender to Krkn, their chaos engineering framework. It analyzes each pod's resource profile from Prometheus metrics and recommends the scenario with the highest probability of causing disruption.

The mechanism is statistical heuristics, not machine learning.

  1. Collect CPU, memory, and network telemetry for each pod from Prometheus while the system is under load.
  2. Detect outliers with a Z-score (how many standard deviations a value sits from the mean) and classify pods that cross a configured threshold as network-intensive, CPU-intensive, or memory-intensive.
  3. Map a chaos scenario to the classified sensitivity. CPU-sensitive services get CPU stress scenarios, memory-sensitive services get memory scenarios.

The recommendation rests on the assumption that the axis under resource stress is the axis that breaks first. Rather than scattering failures at random, telemetry narrows the candidates and raises experiment efficiency.

Krkn-AI, the framework above it, goes a step further and evolves experiments automatically against service level objectives (SLOs). A genetic algorithm generates single and compound failure combinations, scores them by degree of SLO violation, and iteratively refines the high-impact ones. High-damage experiments suited to a given cluster surface without a human guessing at scenarios one by one.

Shift-left testing

A strategy that moves testing to the left, into the early stages of the development cycle.

How to practice shift-left

  1. Static analysis: catch problems as code is written with TypeScript, ESLint, SonarQube
  2. Pre-commit hooks: automated checks before commit with husky + lint-staged
  3. Automated tests on PRs: run tests on every PR in GitHub Actions
  4. Security scanning (Static Application Security Testing, SAST): find vulnerabilities early with Snyk, Semgrep

AI-driven test generation

Where things stand

AI is automating test generation, flake detection, and self-healing selectors.

Main tools (2026)

ToolFunctionCharacteristics
TestimAI-driven E2E test generationSelf-healing selectors
MablIntelligent test automationAutomatic defect detection
Diffblue CoverAutomatic Java unit test generationAI-based code analysis
Codium AILLM-based test generationUnderstands code context
testRigorTests written in natural languageNon-developers can write tests

AI-driven test automation

Approaches where an AI model generates test cases and analyzes the results are spreading:

  • Synthetic data generation: automatically producing test data
  • Bias detection: automatically identifying bias in ML models
  • Explainability testing: verifying that model decisions can be interpreted

Best practices

CI/CD pipeline integration

yaml
# GitHub Actions example
test:
  runs-on: ubuntu-latest
  steps:
    - name: Unit Tests
      run: npm run test:unit -- --coverage
    - name: Integration Tests
      run: npm run test:integration
    - name: E2E Tests (core flows only)
      run: npx playwright test --project=critical
    - name: Mutation Testing (weekly)
      if: github.event.schedule
      run: npx stryker run

Coverage guidelines

MetricTargetNotes
Line coverage80% or higherMinimum bar
Branch coverage70% or higherVerifies conditional branches
Mutation score80% or higherVerifies test quality
E2E coverage100% of core flowsBusiness-critical paths

Principles for writing tests

  1. FIRST: Fast, Isolated, Repeatable, Self-validating, Timely
  2. AAA pattern: Arrange → Act → Assert
  3. One assertion concern per test: the cause is obvious the moment it fails
  4. Express intent in the test name: returns_404_when_the_user_does_not_exist

Summary

Test automation starts with deciding the ratio of unit, integration, and E2E tests. Code with a lot of pure logic fits the test pyramid, while web applications and API servers get a better return from the testing trophy. Contract, mutation, property-based, and snapshot testing fill the gaps the basic levels cannot reach, and chaos engineering confirms resilience under failure.

Shift-left practices and CI/CD integration move verification earlier. AI-driven generation and self-healing selectors reduce the cost of writing and maintaining tests, widening the scope of what gets automated. The next article covers the principles of horizontal scaling and scale-out strategy.

References