How do I verify method calls with Mockito and JUnit?

To verify method calls with Mockito and JUnit, use Mockito’s verify() method. This lets you check whether a mocked dependency method was called, how many times it was called, and what arguments were passed.

1. Basic Example

Suppose you have a service that depends on a repository.

public interface UserRepository {
    void save(User user);
}
public class UserService {
    private final UserRepository userRepository;

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

    public void register(User user) {
        userRepository.save(user);
    }
}

You can verify that save() was called:

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

class UserServiceTest {

    @Test
    void register_shouldSaveUser() {
        UserRepository userRepository = mock(UserRepository.class);
        UserService userService = new UserService(userRepository);

        User user = new User();

        userService.register(user);

        verify(userRepository).save(user);
    }
}

The important line is:

verify(userRepository).save(user);

This means: “After running the test, confirm that save(user) was called on userRepository.”

2. Verifying Number of Calls

By default, verify() expects the method to be called exactly once.

These two lines are equivalent:

verify(userRepository).save(user);
verify(userRepository, times(1)).save(user);

You can verify different call counts:

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

verify(userRepository, times(1)).save(user);
verify(userRepository, times(2)).save(user);
verify(userRepository, never()).delete(user);

Common options include:

verify(mock, times(1)).method();
verify(mock, never()).method();
verify(mock, atLeastOnce()).method();
verify(mock, atLeast(2)).method();
verify(mock, atMost(3)).method();

Example:

import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

class NotificationServiceTest {

    @Test
    void sendWelcomeEmail_shouldNotifyUser() {
        EmailSender emailSender = mock(EmailSender.class);
        NotificationService notificationService = new NotificationService(emailSender);

        notificationService.sendWelcomeEmail("[email protected]");

        verify(emailSender, atLeastOnce()).send("[email protected]", "Welcome!");
    }
}

3. Verifying Arguments with Matchers

If you do not want to match the exact object, you can use argument matchers.

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;

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

For specific values:

verify(emailSender).send(eq("[email protected]"), eq("Welcome!"));

You can also mix broad and specific matching:

verify(emailSender).send(eq("[email protected]"), any(String.class));

Important: if you use matchers for one argument, use matchers for all arguments in that method call.

Correct:

verify(emailSender).send(eq("[email protected]"), any(String.class));

Avoid mixing raw values and matchers:

verify(emailSender).send("[email protected]", any(String.class));

4. Verifying No Calls

To verify that a mock had no interactions:

import static org.mockito.Mockito.verifyNoInteractions;

verifyNoInteractions(userRepository);

To verify that no more calls happened after the expected ones:

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

verify(userRepository).save(user);
verifyNoMoreInteractions(userRepository);

Example:

@Test
void register_invalidUser_shouldNotSaveUser() {
    UserRepository userRepository = mock(UserRepository.class);
    UserService userService = new UserService(userRepository);

    User invalidUser = new User();

    userService.register(invalidUser);

    verifyNoInteractions(userRepository);
}

5. Verifying Order of Calls

Use InOrder when the order matters.

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

import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;

class OrderServiceTest {

    @Test
    void processOrder_shouldCallMethodsInOrder() {
        PaymentService paymentService = mock(PaymentService.class);
        InventoryService inventoryService = mock(InventoryService.class);

        OrderService orderService = new OrderService(paymentService, inventoryService);

        Order order = new Order();

        orderService.process(order);

        InOrder inOrder = inOrder(inventoryService, paymentService);

        inOrder.verify(inventoryService).reserve(order);
        inOrder.verify(paymentService).charge(order);
    }
}

6. Using @Mock with JUnit 5

Instead of manually creating mocks with mock(), you can use Mockito’s JUnit 5 extension.

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 static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void register_shouldSaveUser() {
        User user = new User();

        userService.register(user);

        verify(userRepository).save(user);
    }
}

Here:

@Mock
private UserRepository userRepository;

creates a mock repository.

@InjectMocks
private UserService userService;

creates the service and injects the mock into it.

7. Capturing Arguments with ArgumentCaptor

Use ArgumentCaptor when you want to inspect the object passed to a method.

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

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

class UserServiceTest {

    @Test
    void register_shouldSaveUserWithExpectedName() {
        UserRepository userRepository = mock(UserRepository.class);
        UserService userService = new UserService(userRepository);

        userService.register("Alice");

        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);

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

        User savedUser = userCaptor.getValue();

        assertEquals("Alice", savedUser.getName());
    }
}

This is useful when the method under test creates a new object internally, so you cannot verify using the exact same instance.

8. Verifying Exceptions Still Triggers Calls

You can combine JUnit assertions with Mockito verification.

import org.junit.jupiter.api.Test;

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

class PaymentServiceTest {

    @Test
    void pay_invalidAmount_shouldLogFailure() {
        AuditLogger auditLogger = mock(AuditLogger.class);
        PaymentService paymentService = new PaymentService(auditLogger);

        assertThrows(IllegalArgumentException.class, () -> {
            paymentService.pay(-10);
        });

        verify(auditLogger).log("Invalid payment amount: -10");
    }
}

9. Maven Dependencies

For JUnit 5 and Mockito, add dependencies like these:

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

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.18.0</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.18.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

10. Gradle Dependencies

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
    testImplementation 'org.mockito:mockito-core:5.18.0'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0'

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
    useJUnitPlatform()
}

Common Verification Patterns

verify(repository).save(user);
verify(repository, times(1)).save(user);
verify(repository, never()).delete(user);
verify(repository, atLeastOnce()).save(any(User.class));
verify(repository, atMost(3)).findByEmail(any(String.class));
verifyNoInteractions(repository);
verifyNoMoreInteractions(repository);

Summary

Use Mockito’s verify() when you want to test interactions between objects.

Typical usage:

verify(mock).method(argument);

For example:

verify(userRepository).save(user);

Use verification when the important result of a method is not just a returned value, but that another dependency was called correctly.

How do I use @Mock and @InjectMocks with JUnit?

To use @Mock and @InjectMocks with JUnit, you typically use them with Mockito.

  • @Mock creates a fake/mock dependency.
  • @InjectMocks creates the class under test and injects the mocks into it.
  • With JUnit 5, you enable Mockito using @ExtendWith(MockitoExtension.class).

1. Add Mockito dependencies

Maven

<dependencies>
    <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>
</dependencies>

Gradle

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

test {
    useJUnitPlatform()
}

2. Example class to test

Suppose you have a service that depends on a repository:

public class UserService {
    private final UserRepository userRepository;

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

    public String getUsernameById(Long id) {
        User user = userRepository.findById(id);

        if (user == null) {
            return "Unknown";
        }

        return user.getName();
    }
}

Repository:

public interface UserRepository {
    User findById(Long id);
}

Model:

public class User {
    private final Long id;
    private final String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

3. Use @Mock and @InjectMocks

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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

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

        String result = userService.getUsernameById(1L);

        assertEquals("Alice", result);
    }

    @Test
    void getUsernameByIdReturnsUnknownWhenUserDoesNotExist() {
        when(userRepository.findById(99L))
                .thenReturn(null);

        String result = userService.getUsernameById(99L);

        assertEquals("Unknown", result);
    }
}

How it works

@Mock
private UserRepository userRepository;

This tells Mockito to create a mock implementation of UserRepository.

@InjectMocks
private UserService userService;

This tells Mockito to create a UserService instance and inject the mocked UserRepository into it.

Mockito tries injection in this order:

  1. Constructor injection
  2. Setter injection
  3. Field injection

Constructor injection is usually the best option because it makes dependencies explicit and easier to test.

Verifying mock interactions

You can also verify that a dependency method was called:

import static org.mockito.Mockito.verify;

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

    userService.getUsernameById(1L);

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

Common mistake: forgetting Mockito extension

If you forget this:

@ExtendWith(MockitoExtension.class)

then your @Mock fields may remain null, causing a NullPointerException.

JUnit 4 version

If you are using JUnit 4, use @RunWith(MockitoJUnitRunner.class) instead:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void getUsernameByIdReturnsUserNameWhenUserExists() {
        when(userRepository.findById(1L))
                .thenReturn(new User(1L, "Alice"));

        String result = userService.getUsernameById(1L);

        assertEquals("Alice", result);
    }
}

Quick summary

Use this pattern for JUnit 5:

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private MyRepository repository;

    @InjectMocks
    private MyService service;

    @Test
    void testSomething() {
        when(repository.findSomething()).thenReturn("value");

        String result = service.doSomething();

        assertEquals("value", result);
    }
}

Use @Mock for dependencies and @InjectMocks for the class you are testing.

How do I mock dependencies in unit tests?

To mock dependencies in unit tests, you usually use a mocking framework such as Mockito. Mocking lets you test one class in isolation without running the real logic of its collaborators.

Basic Mockito Example

Suppose you have a service that depends on another class:

@Service
public class MyService {

    private final MyDependency dependency;

    public MyService(MyDependency dependency) {
        this.dependency = dependency;
    }

    public void process() {
        dependency.doSomething();
    }
}

You can mock MyDependency in a unit test like this:

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 static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private MyDependency dependency;

    @InjectMocks
    private MyService myService;

    @Test
    void processCallsDependency() {
        myService.process();

        verify(dependency).doSomething();
    }
}

What the annotations mean

  • @Mock creates a mock object.
  • @InjectMocks creates the class under test and injects the mocks into it.
  • @ExtendWith(MockitoExtension.class) enables Mockito support in JUnit 5.
  • verify(...) checks that a method was called.

Mocking return values

If the dependency returns a value, use when(...).thenReturn(...):

import static org.mockito.Mockito.when;

when(repository.findNameById(1L)).thenReturn("Alice");

Example:

@Test
void returnsMockedValue() {
    when(userRepository.findNameById(1L)).thenReturn("Alice");

    String result = userService.getUserName(1L);

    assertEquals("Alice", result);
}

Mocking exceptions

You can also make a mock throw an exception:

import static org.mockito.Mockito.when;

when(repository.findById(99L))
        .thenThrow(new EntityNotFoundException("User not found"));

For void methods, use doThrow(...):

import static org.mockito.Mockito.doThrow;

doThrow(new RuntimeException("Failure"))
        .when(dependency)
        .doSomething();

Verifying interactions

You can verify how your class interacted with its dependencies:

verify(dependency).doSomething();
verify(dependency, times(1)).doSomething();
verify(dependency, never()).doSomething();

Spring Boot unit test example

For a pure unit test, prefer Mockito without starting the Spring context:

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private MyDependency dependency;

    @InjectMocks
    private MyService service;

    @Test
    void processCallsDependency() {
        service.process();

        verify(dependency).doSomething();
    }
}

Spring integration-style test

If you need the Spring context, use Spring’s test support and replace a bean with a mock:

@SpringBootTest
class MyServiceSpringTest {

    @MockitoBean
    private MyDependency dependency;

    @Autowired
    private MyService service;

    @Test
    void processCallsDependency() {
        service.process();

        verify(dependency).doSomething();
    }
}

Use this style when you want to test Spring wiring, configuration, transactions, security, or other framework behavior.

Rule of thumb

  • Use Mockito @Mock + @InjectMocks for fast unit tests.
  • Use Spring test annotations only when you need the Spring application context.
  • Mock external systems such as databases, APIs, message queues, and file systems.
  • Avoid mocking simple value objects or the class you are actually testing.

How do I use Mockito with JUnit?

To use Mockito with JUnit, you add Mockito to your test dependencies, enable Mockito in your JUnit test class, then create mocks and define their behavior.

Below is a simple JUnit 5 + Mockito example.

1. Add 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 Core -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.14.2</version>
        <scope>test</scope>
    </dependency>

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

Make sure your Maven Surefire plugin 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-core:5.14.2'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.14.2'
}

test {
    useJUnitPlatform()
}

2. Example Class to Test

Suppose you have a service that depends on a repository.

public class User {
    private final Long id;
    private final String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
import java.util.Optional;

public interface UserRepository {
    Optional<User> findById(Long id);
}
public class UserService {
    private final UserRepository userRepository;

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

    public String getUserName(Long id) {
        return userRepository.findById(id)
                .map(User::getName)
                .orElse("Unknown User");
    }
}

3. Write a Mockito Test with JUnit 5

Use @ExtendWith(MockitoExtension.class) to enable Mockito support.

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.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldReturnUserNameWhenUserExists() {
        User user = new User(1L, "Alice");

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

        String result = userService.getUserName(1L);

        assertEquals("Alice", result);
        verify(userRepository).findById(1L);
    }

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

        String result = userService.getUserName(99L);

        assertEquals("Unknown User", result);
        verify(userRepository).findById(99L);
    }
}

4. What the Mockito Annotations Mean

Annotation Purpose
@Mock Creates a mock object
@InjectMocks Creates the class under test and injects mocks into it
@ExtendWith(MockitoExtension.class) Enables Mockito support in JUnit 5

5. Common Mockito Methods

when(...).thenReturn(...)

Used to define mock behavior.

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

verify(...)

Used to check whether a method was called.

verify(userRepository).findById(1L);

verify(..., times(...))

Used to check how many times a method was called.

verify(userRepository, times(1)).findById(1L);

You need this static import:

import static org.mockito.Mockito.times;

6. Mockito Without Annotations

You can also create mocks manually.

import org.junit.jupiter.api.Test;

import java.util.Optional;

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

class UserServiceManualMockTest {

    @Test
    void shouldReturnUserName() {
        UserRepository userRepository = mock(UserRepository.class);
        UserService userService = new UserService(userRepository);

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

        String result = userService.getUserName(1L);

        assertEquals("Alice", result);
    }
}

7. Typical Mockito Test Structure

A clean Mockito test usually follows the Arrange, Act, Assert pattern:

@Test
void shouldReturnUserName() {
    // Arrange
    User user = new User(1L, "Alice");
    when(userRepository.findById(1L)).thenReturn(Optional.of(user));

    // Act
    String result = userService.getUserName(1L);

    // Assert
    assertEquals("Alice", result);
    verify(userRepository).findById(1L);
}

Summary

To use Mockito with JUnit 5:

  1. Add mockito-core and mockito-junit-jupiter.
  2. Add @ExtendWith(MockitoExtension.class) to your test class.
  3. Use @Mock for dependencies.
  4. Use @InjectMocks for the class being tested.
  5. Use when(...).thenReturn(...) to define behavior.
  6. Use verify(...) to check interactions.

How do I write tests for Java records?

Java records are compact classes designed to hold immutable data. Because records automatically provide a constructor, accessor methods, equals(), hashCode(), and toString(), testing them is usually simpler than testing ordinary classes.

In most cases, you do not need to test Java’s generated record behavior directly. Instead, test:

  • custom validation in the compact constructor
  • custom methods you add to the record
  • behavior that depends on equality or immutability
  • serialization/deserialization if the record is used with JSON or persistence frameworks

Example Record

Suppose you have this Java record:

public record User(String username, String email, int age) {

    public User {
        if (username == null || username.isBlank()) {
            throw new IllegalArgumentException("Username must not be blank");
        }

        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Email must be valid");
        }

        if (age < 0) {
            throw new IllegalArgumentException("Age must not be negative");
        }
    }

    public boolean isAdult() {
        return age >= 18;
    }
}

This record has:

  • three components: username, email, and age
  • validation in the compact constructor
  • a custom method named isAdult()

Basic JUnit 5 Test Class

Here is a simple JUnit 5 test class:

import org.junit.jupiter.api.Test;

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

class UserTest {

    @Test
    void shouldCreateUserWithValidData() {
        User user = new User("alice", "[email protected]", 25);

        assertEquals("alice", user.username());
        assertEquals("[email protected]", user.email());
        assertEquals(25, user.age());
    }

    @Test
    void shouldReturnTrueWhenUserIsAdult() {
        User user = new User("bob", "[email protected]", 20);

        assertTrue(user.isAdult());
    }

    @Test
    void shouldReturnFalseWhenUserIsNotAdult() {
        User user = new User("charlie", "[email protected]", 15);

        assertFalse(user.isAdult());
    }

    @Test
    void shouldRejectBlankUsername() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new User("", "[email protected]", 25)
        );

        assertEquals("Username must not be blank", exception.getMessage());
    }

    @Test
    void shouldRejectInvalidEmail() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new User("alice", "invalid-email", 25)
        );

        assertEquals("Email must be valid", exception.getMessage());
    }

    @Test
    void shouldRejectNegativeAge() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new User("alice", "[email protected]", -1)
        );

        assertEquals("Age must not be negative", exception.getMessage());
    }
}

Testing Generated Accessor Methods

Record accessors use the component name directly. For example, if your record is:

public record Product(String name, double price) {
}

The accessors are:

product.name();
product.price();

not:

product.getName();
product.getPrice();

A basic test looks like this:

import org.junit.jupiter.api.Test;

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

class ProductTest {

    @Test
    void shouldExposeRecordComponents() {
        Product product = new Product("Keyboard", 49.99);

        assertEquals("Keyboard", product.name());
        assertEquals(49.99, product.price());
    }
}

However, for plain records with no validation or custom behavior, these tests often provide little value because they only verify Java-generated code.


Testing equals() and hashCode()

Records automatically generate equals() and hashCode() based on all record components.

import org.junit.jupiter.api.Test;

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

class ProductTest {

    @Test
    void shouldCompareRecordsByComponentValues() {
        Product first = new Product("Keyboard", 49.99);
        Product second = new Product("Keyboard", 49.99);
        Product third = new Product("Mouse", 19.99);

        assertEquals(first, second);
        assertEquals(first.hashCode(), second.hashCode());
        assertNotEquals(first, third);
    }
}

Again, you usually do not need this test unless your application depends heavily on equality behavior, such as using records as keys in a Map or elements in a Set.


Testing toString()

Records also generate a readable toString() method:

import org.junit.jupiter.api.Test;

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

class ProductTest {

    @Test
    void shouldGenerateReadableToString() {
        Product product = new Product("Keyboard", 49.99);

        assertEquals("Product[name=Keyboard, price=49.99]", product.toString());
    }
}

Be careful with this kind of test. It can be brittle because it depends on the exact string format.

Test toString() mainly when:

  • you override it
  • logs or messages depend on its output
  • the string representation is part of your expected behavior

Testing Constructor Validation

Records are commonly used with compact constructors for validation.

public record EmailAddress(String value) {

    public EmailAddress {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Email must not be blank");
        }

        if (!value.contains("@")) {
            throw new IllegalArgumentException("Email must contain @");
        }
    }
}

Test both valid and invalid cases:

import org.junit.jupiter.api.Test;

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

class EmailAddressTest {

    @Test
    void shouldCreateEmailAddressWhenValueIsValid() {
        EmailAddress email = new EmailAddress("[email protected]");

        assertEquals("[email protected]", email.value());
    }

    @Test
    void shouldRejectNullEmail() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new EmailAddress(null)
        );

        assertEquals("Email must not be blank", exception.getMessage());
    }

    @Test
    void shouldRejectBlankEmail() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new EmailAddress(" ")
        );

        assertEquals("Email must not be blank", exception.getMessage());
    }

    @Test
    void shouldRejectEmailWithoutAtSign() {
        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> new EmailAddress("invalid-email")
        );

        assertEquals("Email must contain @", exception.getMessage());
    }
}

Using Parameterized Tests for Records

Parameterized tests are useful when a record has multiple invalid input values.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class EmailAddressTest {

    @ParameterizedTest
    @ValueSource(strings = {"", " ", "invalid-email", "user.example.com"})
    void shouldRejectInvalidEmailValues(String value) {
        assertThrows(
                IllegalArgumentException.class,
                () -> new EmailAddress(value)
        );
    }
}

For more complex data, use @CsvSource:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

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

class UserTest {

    @ParameterizedTest
    @CsvSource({
            "'', [email protected], 25",
            "' ', [email protected], 25",
            "alice, invalid-email, 25",
            "alice, [email protected], -1"
    })
    void shouldRejectInvalidUserData(String username, String email, int age) {
        assertThrows(
                IllegalArgumentException.class,
                () -> new User(username, email, age)
        );
    }
}

Testing Custom Methods in Records

If your record contains business logic, test that logic directly.

public record Money(String currency, int amount) {

    public boolean isPositive() {
        return amount > 0;
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency())) {
            throw new IllegalArgumentException("Currencies must match");
        }

        return new Money(currency, amount + other.amount());
    }
}

Tests:

import org.junit.jupiter.api.Test;

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

class MoneyTest {

    @Test
    void shouldReturnTrueForPositiveAmount() {
        Money money = new Money("USD", 100);

        assertTrue(money.isPositive());
    }

    @Test
    void shouldAddMoneyWithSameCurrency() {
        Money first = new Money("USD", 100);
        Money second = new Money("USD", 50);

        Money result = first.add(second);

        assertEquals(new Money("USD", 150), result);
    }

    @Test
    void shouldRejectAddingDifferentCurrencies() {
        Money first = new Money("USD", 100);
        Money second = new Money("EUR", 50);

        IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> first.add(second)
        );

        assertEquals("Currencies must match", exception.getMessage());
    }
}

Testing Immutability

Records are shallowly immutable. This means record components cannot be reassigned, but if a component refers to a mutable object, that object can still be changed.

Example:

import java.util.List;

public record Order(List<String> items) {
}

This record is not deeply immutable:

import java.util.ArrayList;
import java.util.List;

Order order = new Order(new ArrayList<>(List.of("Book")));
order.items().add("Pen");

To make it safer, copy the list:

import java.util.List;

public record Order(List<String> items) {

    public Order {
        items = List.copyOf(items);
    }
}

Then test it:

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

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

class OrderTest {

    @Test
    void shouldDefensivelyCopyItems() {
        List<String> items = new ArrayList<>();
        items.add("Book");

        Order order = new Order(items);

        items.add("Pen");

        assertEquals(List.of("Book"), order.items());
    }

    @Test
    void shouldExposeUnmodifiableItems() {
        Order order = new Order(new ArrayList<>(List.of("Book")));

        assertThrows(
                UnsupportedOperationException.class,
                () -> order.items().add("Pen")
        );
    }
}

This is a valuable test because it verifies your own defensive-copying behavior, not just Java-generated record behavior.


What Should You Actually Test?

For Java records, focus your tests on behavior you wrote yourself.

Good things to test:

Feature Should You Test It? Why
Accessor methods Usually no Generated by Java
equals() / hashCode() Sometimes Useful if equality is important in your domain
toString() Rarely Usually generated and brittle to assert
Compact constructor validation Yes This is your logic
Custom methods Yes This is your logic
Defensive copying Yes Important for immutability
Serialization/deserialization Yes, if used Important for APIs and persistence

Recommended Testing Style

Use clear test names:

@Test
void shouldRejectNegativeAge() {
    // test body
}

Follow the Arrange-Act-Assert pattern:

@Test
void shouldCreateUserWithValidData() {
    // Arrange
    String username = "alice";
    String email = "[email protected]";
    int age = 25;

    // Act
    User user = new User(username, email, age);

    // Assert
    assertEquals(username, user.username());
    assertEquals(email, user.email());
    assertEquals(age, user.age());
}

Summary

To test Java records:

  1. Do not over-test generated code.
  2. Test constructor validation.
  3. Test custom methods.
  4. Test defensive copying for mutable components.
  5. Test JSON or persistence integration only when records are used that way.
  6. Use JUnit 5 assertions such as assertEquals(), assertTrue(), assertFalse(), and assertThrows().

A plain record like this usually needs no dedicated unit test:

public record Point(int x, int y) {
}

But a record like this should be tested:

public record Age(int value) {

    public Age {
        if (value < 0) {
            throw new IllegalArgumentException("Age must not be negative");
        }
    }

    public boolean isAdult() {
        return value >= 18;
    }
}

Because it contains behavior that belongs to your application, not just Java’s generated record features.