Skip to main content
Java · lesson 44 of 66
Open the Java page →
Java intermediate Lesson 44 of 66

Also part of: Software Testing JUnit

Unit Testing with JUnit 5 and Mockito

Learn JUnit 5 and Mockito by running real tests: lifecycle, assertions, assertAll, parameterized tests, stubbing, verify and ArgumentCaptor.

By SkillByExample · Updated

A unit test is a small program that calls your code and checks the result, so that a change which breaks behaviour fails loudly. JUnit 5 finds and runs tests; Mockito replaces a class’s dependencies with fakes you control.

Setup

In a Maven project the two libraries go on the test class path (Surefire 3.x runs JUnit 5 tests with mvn test):

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.14.2</version>
    <scope>test</scope>
</dependency>

To keep each example self-contained, this lesson compiles the files with javac and runs them with the JUnit console launcher, with the jars (junit-platform-console-standalone-1.11.4, mockito-core, mockito-junit-jupiter, byte-buddy, byte-buddy-agent, objenesis) in a lib/ folder. The launcher ends with a timing line and a 12-line counts table; the commands pipe through sed to cut that tail off, and -e junit-jupiter hides two engines these examples don’t use. Each class goes in its own file, named after the class, and the files accumulate in one folder as the lesson goes on — the Mockito section reuses the User record from earlier.

A first test and the lifecycle

public class Calculator {
    int add(int a, int b) { return a + b; }
    int divide(int a, int b) { return a / b; }
}
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.MethodName.class)
class CalculatorTest {
    Calculator calc;

    @BeforeAll static void once()  { System.out.println("@BeforeAll"); }
    @BeforeEach void setUp()       { calc = new Calculator(); System.out.println("@BeforeEach"); }
    @AfterEach void tearDown()     { System.out.println("@AfterEach"); }

    @Test void add_twoPositives_returnsSum() { assertEquals(7, calc.add(3, 4)); }

    @Test void divide_byZero_throws() {
        ArithmeticException e = assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
        assertEquals("/ by zero", e.getMessage());
    }

    @Test @DisplayName("deliberately wrong expectation")
    void add_wrongExpectation() { assertEquals(8, calc.add(3, 4), "3 + 4"); }

    @Test @Disabled("not implemented yet") void subtract() {}
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c CalculatorTest -e junit-jupiter --disable-banner --disable-ansi-colors | sed '/^Test run finished/,$d'
@BeforeAll
@BeforeEach
@AfterEach
@BeforeEach
@AfterEach
@BeforeEach
@AfterEach
╷
└─ JUnit Jupiter ✔
   └─ CalculatorTest ✔
      ├─ add_twoPositives_returnsSum() ✔
      ├─ deliberately wrong expectation ✘ 3 + 4 ==> expected: <8> but was: <7>
      ├─ divide_byZero_throws() ✔
      └─ subtract() ↷ not implemented yet

Failures (1):
  JUnit Jupiter:CalculatorTest:deliberately wrong expectation
    MethodSource [className = 'CalculatorTest', methodName = 'add_wrongExpectation', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: 3 + 4 ==> expected: <8> but was: <7>
       org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
       org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
       org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
       org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
       org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:563)
       CalculatorTest.add_wrongExpectation(CalculatorTest.java:20)
       java.base/java.lang.reflect.Method.invoke(Method.java:580)
       java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
       java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

@BeforeEach and @AfterEach wrap every test, while @BeforeAll ran once (it must be static, because JUnit creates a new CalculatorTest instance per test). assertThrows returns the exception, so its message can be checked too. The failing test shows the "3 + 4" message, the expected and actual values, and the exact line — and the @Disabled test is reported as skipped.

Assertions and assertAll

public record User(String name, int age, String email) {}
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class AssertionsTest {
    User user = new User("Alice", 31, null);

    @Test void individualAsserts() {
        assertEquals("Alice", user.name());
        assertEquals(30, user.age());          // stops here
        assertNotNull(user.email());           // never reached
    }

    @Test void groupedAsserts() {
        assertAll("user",
            () -> assertEquals("Alice", user.name()),
            () -> assertEquals(30, user.age()),
            () -> assertNotNull(user.email(), "email"));
    }

    @Test void otherAssertions() {
        assertEquals(0.3, 0.1 + 0.2, 1e-9);
        assertIterableEquals(List.of("a", "b"), List.of("a", "b"));
        assertArrayEquals(new int[] {1, 2}, new int[] {1, 2});
        assertTrue(user.age() > 18, () -> "built only on failure: " + user);
    }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c AssertionsTest -e junit-jupiter --disable-banner --disable-ansi-colors | sed '/^Test run finished/,$d' | grep -v '^       \(org\|java\)'
╷
└─ JUnit Jupiter ✔
   └─ AssertionsTest ✔
      ├─ groupedAsserts() ✘ user (2 failures)
      │        	org.opentest4j.AssertionFailedError: expected: <30> but was: <31>
      │        	org.opentest4j.AssertionFailedError: email ==> expected: not <null>
      ├─ individualAsserts() ✘ expected: <30> but was: <31>
      └─ otherAssertions() ✔

Failures (2):
  JUnit Jupiter:AssertionsTest:groupedAsserts()
    MethodSource [className = 'AssertionsTest', methodName = 'groupedAsserts', methodParameterTypes = '']
    => org.opentest4j.MultipleFailuresError: user (2 failures)
	org.opentest4j.AssertionFailedError: expected: <30> but was: <31>
	org.opentest4j.AssertionFailedError: email ==> expected: not <null>
       AssertionsTest.groupedAsserts(AssertionsTest.java:15)
       [...]
       Suppressed: org.opentest4j.AssertionFailedError: expected: <30> but was: <31>
         org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
         org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
         org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
         org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
         org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145)
         [...]
       Suppressed: org.opentest4j.AssertionFailedError: email ==> expected: not <null>
         org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
         org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
         org.junit.jupiter.api.AssertNotNull.failNull(AssertNotNull.java:49)
         org.junit.jupiter.api.AssertNotNull.assertNotNull(AssertNotNull.java:35)
         org.junit.jupiter.api.Assertions.assertNotNull(Assertions.java:312)
         [...]
  JUnit Jupiter:AssertionsTest:individualAsserts()
    MethodSource [className = 'AssertionsTest', methodName = 'individualAsserts', methodParameterTypes = '']
    => org.opentest4j.AssertionFailedError: expected: <30> but was: <31>
       AssertionsTest.individualAsserts(AssertionsTest.java:10)

A plain sequence of asserts stops at the first failure, hiding the missing email. assertAll runs every check and reports both problems together. Doubles are compared with a tolerance (1e-9) because 0.1 + 0.2 is not exactly 0.3. The last command also filters out JUnit’s internal stack frames so the failures stay readable.

Parameterized tests

public class FizzBuzz {
    static String of(int n) {
        if (n % 15 == 0) return "FizzBuzz";
        if (n % 3 == 0) return "Fizz";
        if (n % 5 == 0) return "Buzz";
        return String.valueOf(n);
    }
}
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

class FizzBuzzTest {
    @ParameterizedTest
    @ValueSource(ints = {3, 6, 9})
    void multiplesOfThree(int n) { assertEquals("Fizz", FizzBuzz.of(n)); }

    @ParameterizedTest(name = "{0} -> {1}")
    @CsvSource({"5, Buzz", "15, FizzBuzz", "7, 7"})
    void table(int n, String expected) { assertEquals(expected, FizzBuzz.of(n)); }

    static Stream<Arguments> bigNumbers() {
        return Stream.of(Arguments.of(30, "FizzBuzz"), Arguments.of(101, "101"));
    }

    @ParameterizedTest
    @MethodSource("bigNumbers")
    void fromMethod(int n, String expected) { assertEquals(expected, FizzBuzz.of(n)); }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c FizzBuzzTest -e junit-jupiter --disable-banner --disable-ansi-colors | sed '/^Test run finished/,$d'
╷
└─ JUnit Jupiter ✔
   └─ FizzBuzzTest ✔
      ├─ multiplesOfThree(int) ✔
      │  ├─ [1] 3 ✔
      │  ├─ [2] 6 ✔
      │  └─ [3] 9 ✔
      ├─ table(int, String) ✔
      │  ├─ 5 -> Buzz ✔
      │  ├─ 15 -> FizzBuzz ✔
      │  └─ 7 -> 7 ✔
      └─ fromMethod(int, String) ✔
         ├─ [1] 30, FizzBuzz ✔
         └─ [2] 101, 101 ✔

One method becomes many test cases, one per row, each reported separately so a failure names the exact input. @ValueSource supplies single values, @CsvSource rows of arguments (converted to the parameter types), and @MethodSource any objects a static factory returns. name = "{0} -> {1}" controls how each case is labelled.

Mocking with Mockito: stub, then verify

import java.util.Optional;

public interface UserRepository {
    Optional<User> findByEmail(String email);
    User save(User user);
}
public interface EmailService { void sendWelcome(String email); }
public class UserService {
    private final UserRepository repo;
    private final EmailService email;
    public UserService(UserRepository repo, EmailService email) { this.repo = repo; this.email = email; }

    public User register(String name, String address) {
        if (repo.findByEmail(address).isPresent())
            throw new IllegalStateException("already registered: " + address);
        User saved = repo.save(new User(name.strip(), 0, address.toLowerCase()));
        email.sendWelcome(saved.email());
        return saved;
    }
}
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserRepository repo;
    @Mock EmailService email;
    @InjectMocks UserService service;
    @Captor ArgumentCaptor<User> saved;

    @Test void register_newUser_savesNormalisedAndSendsEmail() {
        when(repo.findByEmail("Bob@Example.com")).thenReturn(Optional.empty());
        when(repo.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0));

        service.register("  Bob ", "Bob@Example.com");

        verify(repo).save(saved.capture());
        assertEquals(new User("Bob", 0, "bob@example.com"), saved.getValue());
        verify(email).sendWelcome("bob@example.com");
    }

    @Test void register_duplicate_throwsAndSendsNothing() {
        when(repo.findByEmail("a@b.com")).thenReturn(Optional.of(new User("A", 1, "a@b.com")));
        assertThrows(IllegalStateException.class, () -> service.register("A", "a@b.com"));
        verify(repo, never()).save(any());
        verifyNoInteractions(email);
    }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c UserServiceTest -e junit-jupiter --disable-banner --disable-ansi-colors 2>/dev/null | sed '/^Test run finished/,$d'
╷
└─ JUnit Jupiter ✔
   └─ UserServiceTest ✔
      ├─ register_duplicate_throwsAndSendsNothing() ✔
      └─ register_newUser_savesNormalisedAndSendsEmail() ✔

@Mock creates fakes and @InjectMocks passes them to UserService’s constructor. The test stubs what the repository returns, runs the method, and then verifies the side effects: ArgumentCaptor grabs the exact User that was saved, so the test can check the trimming and lower-casing. never() and verifyNoInteractions prove nothing was saved or sent for a duplicate. The Mockito runs send stderr to /dev/null: on JDK 21 Mockito attaches its bytecode agent at runtime and prints a notice plus several WARNING: A Java agent has been loaded dynamically lines. In a real build, silence them by adding Mockito as a -javaagent in the Surefire argLine.

What a failed verification looks like

import java.util.Optional;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

class VerifyFailureTest {
    @Test void welcomeEmailGoesToTheNormalisedAddress() {
        UserRepository repo = mock(UserRepository.class);
        EmailService email = mock(EmailService.class);
        when(repo.findByEmail(anyString())).thenReturn(Optional.empty());
        when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));

        new UserService(repo, email).register("Cy", "CY@x.io");

        verify(email).sendWelcome("CY@x.io");
    }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c VerifyFailureTest -e junit-jupiter --disable-banner --disable-ansi-colors 2>/dev/null | sed -n '/^Failures/,/^$/p' | head -12
Failures (1):
  JUnit Jupiter:VerifyFailureTest:welcomeEmailGoesToTheNormalisedAddress()
    MethodSource [className = 'VerifyFailureTest', methodName = 'welcomeEmailGoesToTheNormalisedAddress', methodParameterTypes = '']
    => Argument(s) are different! Wanted:
emailService.sendWelcome("CY@x.io");
-> at VerifyFailureTest.welcomeEmailGoesToTheNormalisedAddress(VerifyFailureTest.java:14)
Actual invocations have different arguments:
emailService.sendWelcome("cy@x.io");
-> at UserService.register(UserService.java:10)

Mockito reports what was expected and what actually happened — the call was made, but with cy@x.io. Here the test is wrong, not the code: the service deliberately lower-cases addresses. Reading the “Actual invocations” section is usually the fastest way to tell which one to fix. mock(...) without the extension works too, as this test shows.

Naming and running tests

Name tests method_scenario_expectedResult — register_duplicate_throwsAndSendsNothing tells you what broke without opening the file. In a build tool:

CommandRuns
mvn testall tests
mvn test -Dtest=UserServiceTestone class
mvn test -Dtest='UserServiceTest#register*'matching methods
./gradlew test --tests '*UserServiceTest'one class with Gradle

Practice

  1. Add a @ParameterizedTest with @CsvSource that checks Calculator.add for three rows, including negatives. The solutions reuse the classes already in the folder.
Solution
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class AddTableTest {
    @ParameterizedTest(name = "{0} + {1} = {2}")
    @CsvSource({"1, 2, 3", "-4, 4, 0", "-2, -3, -5"})
    void add(int a, int b, int expected) {
        assertEquals(expected, new Calculator().add(a, b));
    }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c AddTableTest -e junit-jupiter --disable-banner --disable-ansi-colors | sed '/^Test run finished/,$d'
╷
└─ JUnit Jupiter ✔
   └─ AddTableTest ✔
      └─ add(int, int, int) ✔
         ├─ 1 + 2 = 3 ✔
         ├─ -4 + 4 = 0 ✔
         └─ -2 + -3 = -5 ✔
  1. Make the EmailService mock throw RuntimeException("SMTP down") with doThrow(...).when(...) and assert that register propagates it — and that the user was still saved.
Solution
import java.util.Optional;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class SmtpDownTest {
    @Test void register_emailFails_propagates() {
        UserRepository repo = mock(UserRepository.class);
        EmailService email = mock(EmailService.class);
        when(repo.findByEmail(anyString())).thenReturn(Optional.empty());
        when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));
        doThrow(new RuntimeException("SMTP down")).when(email).sendWelcome(anyString());

        RuntimeException e = assertThrows(RuntimeException.class, () -> new UserService(repo, email).register("D", "d@x.io"));
        assertEquals("SMTP down", e.getMessage());
        verify(repo).save(any());
    }
}
$ javac -d out -cp 'lib/*' *.java
$ java -cp 'out:lib/*' org.junit.platform.console.ConsoleLauncher execute -c SmtpDownTest -e junit-jupiter --disable-banner --disable-ansi-colors 2>/dev/null | sed '/^Test run finished/,$d'
╷
└─ JUnit Jupiter ✔
   └─ SmtpDownTest ✔
      └─ register_emailFails_propagates() ✔

Void methods cannot go inside when(...), so stubbing them uses the doThrow(...).when(mock) form. The passing test documents a real design question: the user is saved even though the email failed.

  1. Fix VerifyFailureTest so it passes, by verifying the lower-cased address.
Solution

Change the last line to verify(email).sendWelcome("cy@x.io");. The service lower-cases the address before sending, so that is the call Mockito recorded — exactly what its “Actual invocations” section showed.

Frequently Asked Questions

What is the difference between a unit test and an integration test?
A unit test checks one class in isolation, with slow or external dependencies replaced by mocks or simple fakes, so it runs in milliseconds and fails for exactly one reason. An integration test runs several real components together, such as a service with a real database, to check that they work as a system. Most suites have many unit tests and fewer integration tests.
What is mocking in Java unit tests?
Mocking replaces a real dependency, such as a repository, HTTP client or email sender, with a stand-in object whose behaviour the test controls. With Mockito you stub what a mock returns using when(...).thenReturn(...) and afterwards check how it was called with verify(...). This lets you test a class without a database or network.
When should I not use a mock?
Do not mock value objects, records, collections or the class under test; use the real thing. Avoid mocking types you do not own, such as java.util.List or third-party clients, and wrap them behind your own interface instead. Mock at architectural boundaries where real calls would be slow, nondeterministic or have side effects.
Why does JUnit 5 create a new test class instance for every test?
The default lifecycle is per-method, so each @Test runs on a fresh instance and fields set by one test cannot leak into another. That is why @BeforeEach is used for per-test setup and why @BeforeAll methods must be static. Annotating the class with @TestInstance(Lifecycle.PER_CLASS) switches to one shared instance.
What does assertAll do in JUnit 5?
assertAll runs every assertion you pass it, even after one fails, and then reports all the failures together. A sequence of plain assertEquals calls stops at the first failure, hiding later problems. assertAll is useful when checking several fields of one result, so a single run shows everything that is wrong.