Java Testing Introduction
Why testing matters in Java, the testing pyramid, and an overview of common Java testing frameworks.
Java Testing Introduction
Automated testing is how you prove that code does what you think it does—and keep proving it as the code changes. Instead of running the program by hand and eyeballing the output, you write small programs that exercise your code and check the results automatically. In Java this ecosystem is built around frameworks like JUnit and Mockito, but the underlying ideas—arrange, act, assert—are simple enough to write by hand. This chapter maps the landscape before the later chapters drill into each tool.
Why automated testing matters
A test is a small, repeatable check that a piece of code behaves correctly. The payoff is not the first run—it is every run afterward. Once a behavior is captured in a test, any change that breaks it fails loudly and immediately, instead of surfacing as a bug in production weeks later. Tests also document intent: a well-named test says what the code is supposed to do.
// A test names a behavior, runs the code, and asserts the outcome.
@Test
void addsTwoPositiveNumbers() {
int result = Calculator.add(2, 3);
assertEquals(5, result); // fails the build if result != 5
}The goal is fast feedback. A green test suite means you can refactor with confidence; a red one points straight at what broke.
The testing pyramid
Tests come in layers, usually drawn as a pyramid. Unit tests sit at the bottom: many of them, fast, each checking one class or method in isolation. Integration tests sit in the middle: fewer, slower, checking that components work together (your code plus a database, say). End-to-end (E2E) tests sit at the top: few, slowest, driving the whole application the way a user would.
| Level | Scope | Speed | Count | Java tools |
|---|---|---|---|---|
| Unit | one class/method | fast (ms) | many | JUnit, AssertJ |
| Integration | several components | medium | some | JUnit, Testcontainers |
| End-to-end | whole system | slow | few | Selenium, REST-assured |
The shape matters: lean on cheap, fast unit tests for most coverage, and reserve the slow, brittle E2E tests for a handful of critical user journeys.
The arrange–act–assert pattern
Almost every test, in any framework, follows the same three-step shape. Arrange the inputs and any dependencies. Act by calling the code under test. Assert that the result matches what you expect. Keeping these steps visually separate makes a test easy to read and easy to debug when it fails.
@Test
void rejectsBlankUsername() {
// Arrange
UserService service = new UserService();
// Act
boolean valid = service.isValidUsername(" ");
// Assert
assertFalse(valid);
}A failing assertion throws, the framework records it, and the run continues to the next test—so one broken behavior never hides the others.
JUnit, the standard runner
JUnit is the de facto unit-testing framework for Java. You annotate methods with @Test, JUnit discovers them by reflection, runs each one, and reports pass/fail. Assertions like assertEquals, assertTrue, and assertThrows are static helpers that fail the test when the expectation is not met. Real projects run JUnit through a build tool (Maven's Surefire plugin or Gradle's test task), not by hand.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void dividesNumbers() {
assertEquals(4, Calculator.divide(8, 2));
}
@Test
void throwsOnDivideByZero() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(1, 0));
}
}Because there is no JUnit JAR or build tool on this runner, the example below builds the same idea from scratch—a tiny harness that runs named checks and tallies passes and failures, exactly what @Test plus assertEquals do under the hood.
What to take from the run:
- Each
assertEqualscall is one test case—arrange the inputs, act by callingaddorisBlank, and assert the result—mirroring exactly what a JUnit@Testmethod does. - A passing check prints
PASSand a failing one printsFAILwith both the expected and actual values, which is the diagnostic JUnit's assertion messages give you. - The deliberately wrong case (
expected 10 but got 5) shows what a red test looks like: the harness keeps running the remaining checks instead of stopping at the first failure. - The summary tallies 5 total, 4 passed, 1 failed—the same pass/fail report a test runner prints at the end of a run.
- Because one test failed, the program ends with
BUILD FAILURE, demonstrating why a single broken test should break the whole build in CI.
How the pieces fit
Java's testing tools layer on top of one another, from raw assertions up to full build integration:
- Assertions (
assertEquals,assertThrows) state what must be true. - JUnit discovers and runs
@Testmethods and reports results. - Mockito supplies fake collaborators so a unit can be tested in isolation.
- Maven or Gradle wires the suite into the build, failing the build on any red test.
- CI runs the build on every push, so broken code never reaches the main branch.
Each later chapter takes one rung of this ladder—JUnit annotations and assertions first, then mocking with Mockito, then wiring tests into Maven and Gradle. Understanding where each tool sits keeps the whole testing story coherent.
Practice
In the arrange-act-assert pattern, what does the 'assert' step do?