Skip to main content

Introduction to Software Testing

Every developer has been there: you ship a change, it looks fine locally, and three hours later a bug report lands in your inbox. The feature that broke had nothing to do with what you changed — or so you thought. Automated tests exist to catch exactly these moments before they become incidents.

This guide takes a practical approach to testing across two ecosystems: JavaScript/TypeScript (using Jest and Vitest) and Java (using JUnit 5). Each chapter builds on the previous one, moving from isolated unit tests through integration testing, mocking, TDD, and finally CI pipeline integration.

Why Testing Matters

Shipping software without tests is like driving without a seatbelt: fine until it is not. The arguments for testing are well-documented, but the ones that tend to resonate most with developers are:

Catch regressions automatically. Once you write a test for a bug, that bug can never silently return. The test suite becomes a growing safety net.

Refactor with confidence. When you restructure code, a passing test suite tells you the behaviour is unchanged. Without tests, every refactor is a gamble.

Tests are documentation. A well-named test tells you exactly what a function is supposed to do, with a concrete input and expected output. This is often clearer than a comment.

Reduce manual QA cycles. Automated tests run in seconds. Manually re-testing an entire feature after every change is unsustainable as a codebase grows.

Lower long-term cost. Studies from IBM and others consistently show that bugs found in production cost 10-100x more to fix than bugs found during development. The earlier a bug is caught, the cheaper it is.

The Testing Pyramid

The testing pyramid is a mental model for thinking about the mix of tests you should have. The bottom of the pyramid is cheap and fast; the top is slow and expensive.

/\
/ \
/ E2E\ ← few, slow, full stack
/------\
/ \
/ Integration\ ← some, medium speed
/ \
/----------------\
/ Unit Tests \ ← many, fast, isolated
/____________________\

Unit Tests

A unit test exercises a single function, method, or class in complete isolation. External dependencies — databases, HTTP services, file systems — are replaced with fakes or mocks. Unit tests are the majority of your test suite because they are:

  • Fast (milliseconds each)
  • Deterministic (no network flakiness)
  • Easy to pinpoint when they fail

Integration Tests

Integration tests verify that two or more components work correctly together. A typical example: a service class that calls a repository, which talks to a real (or in-memory) database. These tests are slower and more complex to set up but catch a class of bugs that unit tests miss — mismatched contracts between components.

End-to-End (E2E) Tests

E2E tests drive the entire application stack from the outside, usually through a browser or HTTP client. They are the most faithful representation of real user behaviour but also the most brittle and expensive to run. Tools like Playwright and Cypress sit here.

The Right Mix

A healthy project typically has something like this ratio:

LayerProportionTypical count on a medium project
Unit70–80 %500–2000
Integration15–25 %50–200
E2E5–10 %10–50

These are guidelines, not rules. A microservice with no UI may have no E2E tests at all. A content site with complex rendering may invert the ratio.

The Cost of Bugs Over Time

The cost to fix a defect grows dramatically the later it is found. The rough multipliers that practitioners cite:

Stage foundRelative cost
During coding (dev catches it)
In code review2–5×
In QA / testing phase10–20×
In production50–100×

These numbers vary by project, but the direction is universal. Automated tests move bug discovery as early as possible.

Test Coverage as a Metric

Code coverage measures the percentage of your production code that is executed by your test suite. The common metrics:

  • Line coverage — what percentage of lines were hit at least once
  • Branch coverage — what percentage of conditional branches (if/else, switch arms) were taken
  • Function coverage — what percentage of functions were called
  • Statement coverage — similar to line coverage but counts individual statements

Coverage is a useful signal but not a goal in itself. 100% line coverage does not mean your code is correct — it means every line ran. A test that calls a function but makes no assertions will cover it without verifying anything.

A practical target:

  • < 50% — risky; likely missing large sections of logic
  • 50–70% — survivable for legacy code, but aim higher for new code
  • 70–85% — solid for most production services
  • > 90% — appropriate for libraries and critical path code, but beware diminishing returns

Tooling Overview

This guide uses the following tools. You do not need to install them all now — each chapter covers setup in context.

JavaScript / TypeScript

ToolPurposeNotes
JestTest runner + assertion libraryMost widely used; built-in mocking
VitestTest runner + assertion libraryVite-native, Jest-compatible API, faster HMR
React Testing LibraryComponent testingTests user behaviour, not implementation
PlaywrightE2E browser testingCross-browser, built-in tracing
Istanbul / c8CoverageBundled with Jest; c8 used with Vitest

Java

ToolPurposeNotes
JUnit 5Test frameworkCurrent standard; replaces JUnit 4
MockitoMocking libraryDe-facto standard for Java mocking
AssertJFluent assertionsMore readable than JUnit's Assertions
Spring Boot TestIntegration testing@SpringBootTest, MockMvc
TestcontainersDocker-based integration testingReal databases in tests
JaCoCoCoverageMaven/Gradle plugin

How to Use This Guide

The chapters are designed to be read in order, but each one is self-contained enough to use as a reference. The guide is structured in two parallel tracks that converge on shared concepts:

  1. Chapters 2–5 cover unit testing and mocking in both JavaScript and Java separately.
  2. Chapter 6 covers integration testing for both ecosystems together.
  3. Chapters 7–12 cover higher-level concerns (TDD, React, E2E, coverage, CI) that apply to both.

Code examples are complete and runnable. Each chapter states the dependencies you need to add.

Let's start with unit tests.