W3docs

Java JUnit Introduction

What JUnit is, how to add it to a Java project, and writing your first JUnit test.

Java JUnit Introduction

JUnit is the de facto standard framework for writing automated tests in Java. A test is just a small method that runs a piece of your code and asserts that it behaved as expected; JUnit's job is to discover those methods, run each one in isolation, check the assertions, and report which passed and which failed. The current generation, JUnit 5 (also called JUnit Jupiter), ships as a set of small libraries you add to your build — it is not part of the JDK — and it powers the mvn test / gradle test step that gates almost every Java project's CI.

Why a testing framework at all

You could verify code by hand with main methods and println — but that scales badly. A framework gives you four things you would otherwise rebuild yourself:

  • Discovery — it finds every @Test method automatically; you never maintain a list.
  • Isolation — each test gets a fresh fixture, so one test cannot corrupt another.
  • Assertions — a rich vocabulary (assertEquals, assertThrows, …) that produces precise failure messages.
  • Reporting — a uniform pass/fail summary the build tool and IDE understand.

Get these right once and tests become cheap to write, which is the whole point: cheap tests get written, and code that is tested can be changed without fear.

Adding JUnit to a project

JUnit 5 is a dependency, declared in your build file. With Maven, the junit-jupiter aggregator pulls in the API (to compile against) and the engine (to run with):

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.11.3</version>
  <scope>test</scope>
</dependency>

With Gradle it is two lines plus the useJUnitPlatform() switch:

dependencies {
  testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
  useJUnitPlatform()
}

Test sources live under src/test/java, mirroring the package of the class they exercise. The test scope keeps JUnit out of your production artifact.

Your first test

A JUnit test is an ordinary method annotated with @Test. Inside it you call the code under test and assert on the result. Here is a Calculator and a test class for it:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class CalculatorTest {

  private final Calculator calc = new Calculator();

  @Test
  void addReturnsSum() {
    assertEquals(5, calc.add(2, 3));
  }

  @Test
  void divideByZeroThrows() {
    assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
  }
}

Notice the pattern every test follows — Arrange a fixture, Act by calling the method, Assert the outcome. Test methods are package-private (no public needed in JUnit 5) and return void. Run mvn test and the build goes green only if every assertion holds.

The annotations and assertions you will use most

JUnit's surface area is small. These few members cover the vast majority of real tests:

MemberPackage / classPurpose
@Testorg.junit.jupiter.apiMarks a method as a test
@BeforeEach / @AfterEachorg.junit.jupiter.apiRun before/after every test (set up / tear down fixtures)
@BeforeAll / @AfterAllorg.junit.jupiter.apiRun once before/after all tests in the class
@DisplayNameorg.junit.jupiter.apiA human-readable name for reports
@Disabledorg.junit.jupiter.apiTemporarily skip a test
assertEquals(exp, act)AssertionsFail unless the two are equal
assertTrue / assertFalseAssertionsFail unless a boolean holds
assertThrows(type, exec)AssertionsFail unless the lambda throws that exception
assertNull / assertNotNullAssertionsFail on the wrong nullness

@BeforeEach is what gives every test a clean slate — JUnit constructs a fresh instance of the test class for each @Test, then runs the setup, so state never leaks between tests.

What JUnit does for you, in one runnable file

The code runner here has no JUnit on its classpath (it is an external library, not part of the JDK), so the example below re-implements JUnit's core loop in plain Java: a fixture re-created before each test, a small set of assertXxx helpers, a list of test methods run independently, and a pass/fail tally at the end. This is exactly the machinery JUnit automates — seeing it bare makes the real framework obvious. One test fails on purpose so you can see what red looks like.

java— editable, runs on the server

What to take from the run:

  • The three sound tests print PASS and the fourth prints FAIL deliberatelyFailing -> expected <10> but was <5> — a precise failure message, not just "a test failed." That diff (expected … but was …) is exactly what JUnit's assertEquals gives you, and it is what makes a red test diagnosable at a glance.
  • setUp() runs before every test, so calc is a fresh Calculator each time. This is the @BeforeEach contract: tests are isolated, and the order they run in can never matter because none of them shares mutable state.
  • divideThrowsOnZero passes by asserting that an exception is thrown — assertThrows makes "this should fail" a first-class, positive assertion instead of a fragile try/catch. Expected exceptions are behavior worth testing, not errors to swallow.
  • The final tally — Tests run: 4, Passed: 3, Failed: 1, Assertions: 5 — is the report. One failing test out of four still flips the whole build to RED; CI treats any failure as a stop, which is why a green suite is meaningful.
  • Nothing here imported JUnit, yet the shape is identical: annotated-method discovery, per-test setup, assertions, summary. JUnit's value is that it automates this loop (and adds discovery, parallelism, parameterized tests, and IDE integration) so you write only the test bodies.

What the rest of this part covers

Writing and organizing test classes, the full lifecycle annotations, the complete assertion catalogue, parameterized and repeated tests, assumptions, exception and timeout testing, and mocking collaborators with Mockito. The next chapter starts where every suite begins: defining a test class and running it.

Practice

Practice

In JUnit 5, what does annotating a method with @BeforeEach guarantee about the test fixture?