How do I compare objects in JUnit tests?

In JUnit tests, you usually compare objects with assertions, not with ==.

The most common choices are:

  • assertEquals(expected, actual) — compares objects using their equals() method.
  • assertSame(expected, actual) — checks whether both references point to the same object.
  • AssertJ’s assertThat(actual).isEqualTo(expected) — a more fluent alternative.
  • Field-by-field assertions — useful when you only care about some properties.

1. Comparing Objects with assertEquals()

In JUnit 5, use:

import org.junit.jupiter.api.Test;

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

class PersonTest {

    @Test
    void shouldCompareObjectsUsingEquals() {
        Person expected = new Person("Alice", 30);
        Person actual = new Person("Alice", 30);

        assertEquals(expected, actual);
    }
}

This works only if Person correctly overrides equals().

Example:

import java.util.Objects;

public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof Person person)) {
            return false;
        }

        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

Now two different Person instances with the same values are considered equal.

2. Do Not Use == for Value Comparison

This checks whether both variables refer to the same object in memory:

Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);

assertEquals(p1, p2); // compares values using equals()

But this would fail:

import static org.junit.jupiter.api.Assertions.assertTrue;

assertTrue(p1 == p2); // false, because they are different objects

Use == only when you intentionally want to check reference identity.

3. Use assertSame() for Reference Comparison

If you want to verify that two references point to the exact same object, use assertSame():

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertSame;

class PersonTest {

    @Test
    void shouldReferToSameObject() {
        Person person = new Person("Alice", 30);

        Person samePerson = person;

        assertSame(person, samePerson);
    }
}

Use assertSame() for identity checks, not regular value comparison.

4. Compare Individual Fields

Sometimes you do not need full object equality. You may only care about specific fields:

import org.junit.jupiter.api.Test;

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

class PersonTest {

    @Test
    void shouldComparePersonFields() {
        Person actual = new Person("Alice", 30);

        assertEquals("Alice", actual.getName());
        assertEquals(30, actual.getAge());
    }
}

This is useful when:

  • the class does not override equals(),
  • only some fields matter,
  • generated fields like id, timestamps, or audit fields should be ignored.

5. Use AssertJ for More Readable Object Assertions

AssertJ provides fluent assertions:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class PersonTest {

    @Test
    void shouldCompareObjectsWithAssertJ() {
        Person expected = new Person("Alice", 30);
        Person actual = new Person("Alice", 30);

        assertThat(actual).isEqualTo(expected);
    }
}

You can also compare specific properties:

assertThat(actual)
        .extracting(Person::getName, Person::getAge)
        .containsExactly("Alice", 30);

Or compare objects recursively:

assertThat(actual)
        .usingRecursiveComparison()
        .isEqualTo(expected);

Recursive comparison is helpful when objects contain nested objects and you do not want to rely on every class implementing equals().

6. Comparing Lists of Objects

For lists, assertEquals() also uses equals() on each element:

import org.junit.jupiter.api.Test;

import java.util.List;

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

class PersonListTest {

    @Test
    void shouldCompareListsOfPeople() {
        List<Person> expected = List.of(
                new Person("Alice", 30),
                new Person("Bob", 25)
        );

        List<Person> actual = List.of(
                new Person("Alice", 30),
                new Person("Bob", 25)
        );

        assertEquals(expected, actual);
    }
}

The order must match. If order does not matter, AssertJ is often clearer:

assertThat(actual)
        .containsExactlyInAnyOrderElementsOf(expected);

7. Common Rule of Thumb

Use this:

assertEquals(expected, actual);

when you want to compare object values.

Use this:

assertSame(expected, actual);

when you want to check that both variables refer to the exact same object.

Avoid this for object value comparison:

expected == actual

because it checks identity, not logical equality.

Summary

Goal Recommended Assertion
Compare object values assertEquals(expected, actual)
Compare object references assertSame(expected, actual)
Compare selected fields assertEquals(expectedName, actual.getName())
Compare nested objects without relying on equals() AssertJ usingRecursiveComparison()
Compare lists in order assertEquals(expectedList, actualList)
Compare lists ignoring order AssertJ containsExactlyInAnyOrderElementsOf()

In most JUnit tests, object comparison starts with assertEquals(expected, actual), as long as the class has a proper equals() implementation.

How do I use AssertJ with JUnit for better assertions?

You can use AssertJ with JUnit to write more readable, fluent assertions than standard JUnit assertions.

1. Add AssertJ Dependency

If you use Maven:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.26.3</version>
    <scope>test</scope>
</dependency>

If you use Gradle:

testImplementation("org.assertj:assertj-core:3.26.3")

2. Use AssertJ in a JUnit Test

Import assertThat statically:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UserServiceTest {

    @Test
    void shouldReturnUserName() {
        String name = "Alice";

        assertThat(name)
                .isNotNull()
                .startsWith("A")
                .endsWith("e")
                .hasSize(5);
    }
}

3. AssertJ vs. JUnit Assertions

JUnit:

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

assertEquals("Alice", name);
assertTrue(name.startsWith("A"));

AssertJ:

import static org.assertj.core.api.Assertions.assertThat;

assertThat(name)
        .isEqualTo("Alice")
        .startsWith("A");

AssertJ reads more naturally and usually gives better failure messages.

4. Common AssertJ Examples

Strings

assertThat(email)
        .isNotBlank()
        .contains("@")
        .endsWith(".com");

Numbers

assertThat(price)
        .isPositive()
        .isGreaterThan(10)
        .isLessThanOrEqualTo(100);

Collections

assertThat(users)
        .hasSize(3)
        .extracting(User::getName)
        .containsExactly("Alice", "Bob", "Charlie");

Objects

assertThat(user)
        .isNotNull()
        .extracting(User::getName, User::getEmail)
        .containsExactly("Alice", "[email protected]");

Exceptions

assertThatThrownBy(() -> userService.findById(-1L))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("id");

Or with JUnit + AssertJ:

IllegalArgumentException exception =
        assertThrows(IllegalArgumentException.class, () -> userService.findById(-1L));

assertThat(exception)
        .hasMessageContaining("id");

5. Example with Spring/JUnit Test

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CalculatorTest {

    @Test
    void shouldAddNumbers() {
        Calculator calculator = new Calculator();

        int result = calculator.add(2, 3);

        assertThat(result).isEqualTo(5);
    }
}

6. Helpful AssertJ Tips

Use as() to describe assertions:

assertThat(user.getEmail())
        .as("user email should be valid")
        .contains("@");

Use recursive comparison for objects:

assertThat(actualUser)
        .usingRecursiveComparison()
        .isEqualTo(expectedUser);

Use containsExactlyInAnyOrder() when order does not matter:

assertThat(roles)
        .containsExactlyInAnyOrder("ADMIN", "USER");

Recommended Pattern

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@Test
void shouldDoSomething() {
    // arrange
    User user = new User("Alice");

    // act
    String result = user.getName();

    // assert
    assertThat(result).isEqualTo("Alice");
}

In short: add assertj-core, statically import assertThat, and replace basic JUnit assertions with fluent AssertJ assertions for clearer and more expressive tests.

How do I test repository-like classes without a real database?

You can test repository-like classes without a real database by replacing the database dependency with a fake, mock, or in-memory implementation, depending on what you want to verify.

1. Use mocks for unit tests

If your class depends on a repository interface, mock it and verify behavior without touching a database.

Example with JUnit 5 and Mockito:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

class UserServiceTest {

    @Test
    void returnsUserName() {
        UserRepository userRepository = Mockito.mock(UserRepository.class);

        when(userRepository.findById(1L))
                .thenReturn(Optional.of(new User(1L, "Alice")));

        UserService service = new UserService(userRepository);

        String name = service.getUserName(1L);

        assertThat(name).isEqualTo("Alice");
    }
}

This is best when testing service logic, not repository implementation details.


2. Use a fake in-memory repository

If you have repository-like classes that are simple abstractions, create an in-memory fake.

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

class InMemoryUserRepository implements UserRepository {

    private final Map<Long, User> users = new HashMap<>();

    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(users.get(id));
    }

    @Override
    public User save(User user) {
        users.put(user.id(), user);
        return user;
    }
}

Then use it in tests:

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class UserServiceTest {

    @Test
    void savesAndLoadsUser() {
        UserRepository repository = new InMemoryUserRepository();
        UserService service = new UserService(repository);

        service.createUser(1L, "Alice");

        assertThat(service.getUserName(1L)).isEqualTo("Alice");
    }
}

This is useful when you want tests that are more realistic than mocks but still fast.


3. Use Spring @MockBean / @MockitoBean in Spring tests

For Spring MVC or service-layer tests, replace a repository bean with a mock.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Optional;

import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class UserServiceSpringTest {

    @MockitoBean
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Test
    void returnsUserName() {
        when(userRepository.findById(1L))
                .thenReturn(Optional.of(new User(1L, "Alice")));

        assertThat(userService.getUserName(1L)).isEqualTo("Alice");
    }
}

Use this when you want Spring wiring but not database access.


4. Use @DataJpaTest with an embedded database

If you are testing a Spring Data JPA repository itself, mocks are usually not enough. You need to verify queries, mappings, transactions, and entity relationships.

For that, use an embedded database such as H2:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void findsByEmail() {
        User user = new User();
        user.setName("Alice");
        user.setEmail("[email protected]");

        userRepository.save(user);

        User found = userRepository.findByEmail("[email protected]").orElseThrow();

        assertThat(found.getName()).isEqualTo("Alice");
    }
}

This does use a database, but not a “real” external one. It is fast and isolated.


5. Use Testcontainers for production-like repository tests

If your repository uses database-specific features, H2 may behave differently from PostgreSQL, MySQL, Oracle, etc.

In that case, use Testcontainers:

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@DataJpaTest
@Testcontainers
class UserRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>("postgres:16");

    @Test
    void testRepository() {
        // repository test here
    }
}

This is not a local “real database” you manage manually, but it gives you much higher confidence.


Recommended approach

What you are testing Recommended technique
Service using a repository Mock repository
Business logic with persistence-like behavior Fake in-memory repository
Spring wiring without DB @MockitoBean
JPA mappings and queries @DataJpaTest
DB-specific SQL/features Testcontainers

Rule of thumb

Do not unit test Spring Data JPA repositories by mocking JPA internals like EntityManager unless your repository has significant custom logic.

Instead:

  • Mock repositories when testing services.
  • Use fakes when testing domain/application logic.
  • Use @DataJpaTest or Testcontainers when testing actual persistence behavior.

How do I test service classes with JUnit and Mockito?

Testing Service Classes with JUnit and Mockito

Service classes are usually where your business logic lives. They often depend on repositories, clients, mappers, validators, or other services.

When unit testing a service, the goal is usually:

  • test the service logic itself
  • mock external dependencies
  • avoid starting the Spring container unless necessary
  • verify returned values, exceptions, and interactions

For most service unit tests, you can use JUnit 5 with Mockito.


1. Example Service Class

Suppose you have a service that creates and retrieves users.

package com.example.user;

import java.util.Optional;

public class UserService {

    private final UserRepository userRepository;
    private final EmailValidator emailValidator;

    public UserService(UserRepository userRepository, EmailValidator emailValidator) {
        this.userRepository = userRepository;
        this.emailValidator = emailValidator;
    }

    public User createUser(String name, String email) {
        if (!emailValidator.isValid(email)) {
            throw new IllegalArgumentException("Invalid email address");
        }

        if (userRepository.existsByEmail(email)) {
            throw new IllegalStateException("Email already exists");
        }

        User user = new User(null, name, email);
        return userRepository.save(user);
    }

    public User getUserById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }
}

Supporting classes might look like this:

package com.example.user;

public record User(Long id, String name, String email) {
}
package com.example.user;

import java.util.Optional;

public interface UserRepository {
    boolean existsByEmail(String email);

    User save(User user);

    Optional<User> findById(Long id);
}
package com.example.user;

public interface EmailValidator {
    boolean isValid(String email);
}
package com.example.user;

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

2. Add JUnit and Mockito Dependencies

Maven

<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.11.4</version>
        <scope>test</scope>
    </dependency>

    <!-- Mockito + JUnit 5 integration -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.15.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>

If you use Maven, also make sure Surefire supports JUnit 5:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
        </plugin>
    </plugins>
</build>

Gradle

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.15.2'
}

test {
    useJUnitPlatform()
}

3. Basic Service Test with Mockito

Use @ExtendWith(MockitoExtension.class) to enable Mockito in JUnit 5.

package com.example.user;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailValidator emailValidator;

    @InjectMocks
    private UserService userService;

    @Test
    void createUser_WithValidEmailAndNewEmail_ReturnsSavedUser() {
        // Arrange
        String name = "Alice";
        String email = "[email protected]";

        User savedUser = new User(1L, name, email);

        when(emailValidator.isValid(email)).thenReturn(true);
        when(userRepository.existsByEmail(email)).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // Act
        User result = userService.createUser(name, email);

        // Assert
        assertEquals(1L, result.id());
        assertEquals("Alice", result.name());
        assertEquals("[email protected]", result.email());

        verify(emailValidator).isValid(email);
        verify(userRepository).existsByEmail(email);
        verify(userRepository).save(any(User.class));
    }

    @Test
    void createUser_WithInvalidEmail_ThrowsException() {
        // Arrange
        String email = "invalid-email";

        when(emailValidator.isValid(email)).thenReturn(false);

        // Act
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> userService.createUser("Alice", email)
        );

        // Assert
        assertEquals("Invalid email address", exception.getMessage());

        verify(emailValidator).isValid(email);
        verify(userRepository, never()).existsByEmail(email);
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void createUser_WithExistingEmail_ThrowsException() {
        // Arrange
        String email = "[email protected]";

        when(emailValidator.isValid(email)).thenReturn(true);
        when(userRepository.existsByEmail(email)).thenReturn(true);

        // Act
        IllegalStateException exception = assertThrows(
                IllegalStateException.class,
                () -> userService.createUser("Alice", email)
        );

        // Assert
        assertEquals("Email already exists", exception.getMessage());

        verify(emailValidator).isValid(email);
        verify(userRepository).existsByEmail(email);
        verify(userRepository, never()).save(any(User.class));
    }

    @Test
    void getUserById_WhenUserExists_ReturnsUser() {
        // Arrange
        User user = new User(1L, "Alice", "[email protected]");

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        // Act
        User result = userService.getUserById(1L);

        // Assert
        assertEquals(user, result);

        verify(userRepository).findById(1L);
    }

    @Test
    void getUserById_WhenUserDoesNotExist_ThrowsException() {
        // Arrange
        when(userRepository.findById(99L)).thenReturn(Optional.empty());

        // Act
        UserNotFoundException exception = assertThrows(
                UserNotFoundException.class,
                () -> userService.getUserById(99L)
        );

        // Assert
        assertTrue(exception.getMessage().contains("99"));

        verify(userRepository).findById(99L);
    }
}

4. What @Mock and @InjectMocks Do

@Mock

Creates fake versions of dependencies.

@Mock
private UserRepository userRepository;

This means you control what the repository returns:

when(userRepository.findById(1L))
        .thenReturn(Optional.of(user));

@InjectMocks

Creates the service under test and injects the mocks into it.

@InjectMocks
private UserService userService;

Mockito will try constructor injection first, which works well if your service uses constructor injection.


5. Typical Test Structure

A clean service test usually follows Arrange, Act, Assert:

@Test
void methodName_StateUnderTest_ExpectedBehavior() {
    // Arrange
    when(repository.findById(1L)).thenReturn(Optional.of(entity));

    // Act
    Result result = service.method(1L);

    // Assert
    assertEquals(expected, result);
    verify(repository).findById(1L);
}

6. Testing Exceptions

Use assertThrows() when the service should reject invalid input or missing data.

@Test
void getUserById_WhenUserMissing_ThrowsException() {
    when(userRepository.findById(1L)).thenReturn(Optional.empty());

    UserNotFoundException exception = assertThrows(
            UserNotFoundException.class,
            () -> userService.getUserById(1L)
    );

    assertEquals("User not found: 1", exception.getMessage());
}

7. Verifying Repository Calls

Mockito can check whether a dependency method was called.

verify(userRepository).findById(1L);

You can also verify that something was not called:

verify(userRepository, never()).save(any(User.class));

This is useful when testing validation failures.


8. Capturing Arguments

Sometimes you need to inspect the object passed to a mocked dependency.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class UserServiceArgumentCaptorTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailValidator emailValidator;

    @InjectMocks
    private UserService userService;

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    void createUser_PassesCorrectUserToRepository() {
        // Arrange
        when(emailValidator.isValid("[email protected]")).thenReturn(true);
        when(userRepository.existsByEmail("[email protected]")).thenReturn(false);
        when(userRepository.save(any(User.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));

        // Act
        userService.createUser("Alice", "[email protected]");

        // Assert
        verify(userRepository).save(userCaptor.capture());

        User capturedUser = userCaptor.getValue();

        assertEquals("Alice", capturedUser.name());
        assertEquals("[email protected]", capturedUser.email());
    }
}

Additional imports needed for this example:

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

9. When Should You Use @SpringBootTest?

For normal service unit tests, you usually do not need this:

@SpringBootTest

@SpringBootTest starts the Spring application context, which makes tests slower and more integration-style.

Use plain Mockito tests when you want to test only the service logic:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
}

Use @SpringBootTest when you want to test that Spring wiring, configuration, transactions, database integration, or multiple beans work together.


10. Service Test Checklist

When testing service classes:

  • Mock repositories and external clients.
  • Use the real service class.
  • Test successful paths.
  • Test validation failures.
  • Test missing data scenarios.
  • Test exception paths.
  • Verify important dependency calls.
  • Avoid testing getters, setters, or framework behavior.
  • Avoid starting Spring unless you need integration testing.
  • Prefer constructor injection in your service classes.

Summary

To test service classes with JUnit and Mockito:

  1. Add junit-jupiter and mockito-junit-jupiter.
  2. Annotate the test with @ExtendWith(MockitoExtension.class).
  3. Mock dependencies with @Mock.
  4. Create the service with @InjectMocks.
  5. Stub dependency behavior with when(...).thenReturn(...).
  6. Call the service method.
  7. Assert the result with JUnit assertions.
  8. Verify interactions with Mockito when useful.

For most service classes, this gives you fast, focused, and reliable unit tests.

How do I mock exceptions in JUnit tests?

In JUnit tests, you usually don’t “mock” exceptions directly. Instead, you either:

  1. Assert that real code throws an exception, or
  2. Configure a mock dependency to throw an exception.

1. Assert that code throws an exception

With JUnit 5, use assertThrows.

import org.junit.jupiter.api.Test;

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

class MyServiceTest {

    @Test
    void shouldThrowException() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> {
                    throw new IllegalArgumentException("Invalid input");
                }
        );

        assertEquals("Invalid input", exception.getMessage());
    }
}

assertThrows verifies that the code inside the lambda throws the expected exception type.

2. Mock a dependency to throw an exception with Mockito

If your class depends on another object, you can configure the mock to throw an exception.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

class UserServiceTest {

    @Test
    void shouldThrowWhenRepositoryFails() {
        UserRepository repository = Mockito.mock(UserRepository.class);

        when(repository.findById(1L))
                .thenThrow(new RuntimeException("Database error"));

        UserService service = new UserService(repository);

        assertThrows(
                RuntimeException.class,
                () -> service.getUser(1L)
        );
    }
}

3. Mock exceptions for void methods

For void methods, use doThrow.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doThrow;

class NotificationServiceTest {

    @Test
    void shouldThrowWhenEmailFails() {
        EmailClient emailClient = Mockito.mock(EmailClient.class);

        doThrow(new RuntimeException("Email failed"))
                .when(emailClient)
                .sendEmail("[email protected]");

        NotificationService service = new NotificationService(emailClient);

        assertThrows(
                RuntimeException.class,
                () -> service.notifyUser("[email protected]")
        );
    }
}

4. Check the exception message

You can capture the thrown exception and verify its message.

import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void shouldThrowArithmeticException() {
        ArithmeticException exception = assertThrows(
                ArithmeticException.class,
                () -> {
                    int result = 10 / 0;
                }
        );

        assertEquals("/ by zero", exception.getMessage());
    }
}

5. JUnit 4 alternative

If you are using JUnit 4, you can use the expected attribute.

import org.junit.Test;

public class CalculatorTest {

    @Test(expected = ArithmeticException.class)
    public void shouldThrowArithmeticException() {
        int result = 10 / 0;
    }
}

However, in JUnit 4, ExpectedException or AssertJ is better if you need to check the message.

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class CalculatorTest {

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Test
    public void shouldThrowWithMessage() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid input");

        throw new IllegalArgumentException("Invalid input");
    }
}

Quick rule of thumb

  • Use assertThrows when testing your own method throws an exception.
  • Use when(...).thenThrow(...) for mocked methods that return a value.
  • Use doThrow(...).when(...) for mocked void methods.

For modern Java projects, prefer JUnit 5 + Mockito:

assertThrows(SomeException.class, () -> service.method());