JavaUnit Testing (JUnit)

Unit Testing (JUnit)

Manually running a program and eyeballing its output doesn't scale — it's slow, it's easy to forget a case, and it gives you no protection against a future change quietly breaking something that used to work. Automated tests fix this: you write code that exercises your code and checks the result, and you can rerun that check in seconds, as often as you like, for the entire life of the project.

JUnit is the standard testing framework for Java. The current generation, JUnit 5 (also called JUnit Jupiter), is what virtually all modern Java projects use, and it is what Maven and Gradle both support out of the box.
Writing a Test
A test is just a method annotated with @Test. Test classes are typically named after the class they test, with a "Test" suffix, and live under src/test/java.

Calculator.java

Java
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return a / b;
    }
}

CalculatorTest.java

Java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    @Test
    void addsTwoNumbers() {
        Calculator calculator = new Calculator();
        assertEquals(5, calculator.add(2, 3));
    }

    @Test
    void divideByZeroThrows() {
        Calculator calculator = new Calculator();
        assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0));
    }
}
Common Assertion Methods

Method

Purpose

assertEquals(expected, actual)

Fails unless the two values are equal

assertTrue(condition)

Fails unless the given boolean expression is true

assertThrows(Type.class, () -> ...)

Fails unless the supplied code throws an exception of the given type

assertNotNull(value)

Fails if the value is null

Setup and Teardown with @BeforeEach / @AfterEach

Many tests need the same setup — a fresh object, a database connection, a temporary file — before each test runs, and often some cleanup afterward. Rather than repeating that code in every test method, JUnit lets you factor it into lifecycle methods that run automatically around each test.

Sharing setup across tests

Java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculatorLifecycleTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator(); // runs before every @Test method
    }

    @AfterEach
    void tearDown() {
        calculator = null; // runs after every @Test method
    }

    @Test
    void addsTwoNumbers() {
        assertEquals(7, calculator.add(3, 4));
    }
}
  • @BeforeEach runs before every test method in the class — ideal for creating a fresh object under test

  • @AfterEach runs after every test method — useful for closing resources opened during setup

  • @BeforeAll / @AfterAll (static methods) run once for the whole class, for expensive one-time setup

Note
For larger applications, tests often need to isolate a class from its real collaborators — a database, a network call, another service. That's the job of a mocking library like Mockito, which lets you create fake versions of dependencies that return canned responses instead of doing real work. It pairs naturally with JUnit and is worth learning once you're comfortable writing plain unit tests.
Tip
Name test methods after the behavior they verify, not the method under test — divideByZeroThrows reads far better as a failure message in a report than testDivide2.