How do I test Collections, Lists, Maps, and Sets in JUnit?

Testing Collections in JUnit

In JUnit, you usually test collections with assertions such as:

  • assertEquals
  • assertTrue
  • assertFalse
  • assertIterableEquals
  • assertArrayEquals
  • assertThrows

If you are using JUnit 5, import assertions from:

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

1. Testing a List

Use assertEquals when order matters.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ListTest {

    @Test
    void shouldContainExpectedItemsInOrder() {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        assertEquals(3, names.size());
        assertEquals("Alice", names.get(0));
        assertEquals(List.of("Alice", "Bob", "Charlie"), names);
        assertTrue(names.contains("Bob"));
    }
}

List.equals() checks:

  1. Same size
  2. Same elements
  3. Same order

So this works well:

assertEquals(List.of("Alice", "Bob"), actualList);

2. Testing a Set

A Set does not guarantee order, so compare it with another set.

import org.junit.jupiter.api.Test;

import java.util.Set;

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

class SetTest {

    @Test
    void shouldContainExpectedUniqueItems() {
        Set<String> roles = Set.of("ADMIN", "USER");

        assertEquals(2, roles.size());
        assertTrue(roles.contains("ADMIN"));
        assertEquals(Set.of("USER", "ADMIN"), roles);
    }
}

Set.equals() ignores order, so this passes:

assertEquals(Set.of("ADMIN", "USER"), actualSet);

3. Testing a Map

Use assertEquals to compare maps by key/value pairs.

import org.junit.jupiter.api.Test;

import java.util.Map;

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

class MapTest {

    @Test
    void shouldContainExpectedEntries() {
        Map<String, Integer> scores = Map.of(
                "Alice", 95,
                "Bob", 88
        );

        assertEquals(2, scores.size());
        assertEquals(95, scores.get("Alice"));
        assertTrue(scores.containsKey("Bob"));
        assertEquals(Map.of("Bob", 88, "Alice", 95), scores);
    }
}

Map.equals() checks that both maps contain the same mappings, regardless of entry order.

assertEquals(Map.of("Alice", 95, "Bob", 88), actualMap);

4. Testing Collection Size

import org.junit.jupiter.api.Test;

import java.util.List;

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

class CollectionSizeTest {

    @Test
    void shouldHaveExpectedSize() {
        List<String> items = List.of("A", "B", "C");

        assertEquals(3, items.size());
    }
}

5. Testing Empty Collections

import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

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

class EmptyCollectionTest {

    @Test
    void shouldBeEmpty() {
        List<String> items = Collections.emptyList();

        assertTrue(items.isEmpty());
    }
}

You can also use:

assertEquals(0, items.size());

But this is usually more readable:

assertTrue(items.isEmpty());

6. Testing That a Collection Contains an Item

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ContainsTest {

    @Test
    void shouldContainExpectedItem() {
        List<String> names = List.of("Alice", "Bob");

        assertTrue(names.contains("Alice"));
    }
}

For negative checks:

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

// ...

assertFalse(names.contains("Charlie"));

7. Testing List Order Explicitly

You can use assertIterableEquals for lists and other iterable collections.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class IterableTest {

    @Test
    void shouldMatchExpectedOrder() {
        List<String> actual = List.of("A", "B", "C");

        assertIterableEquals(List.of("A", "B", "C"), actual);
    }
}

This checks both contents and order.


8. Testing Same Contents Regardless of Order

For lists where order does not matter, convert both to sets:

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Set;

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

class UnorderedListTest {

    @Test
    void shouldHaveSameItemsIgnoringOrder() {
        List<String> actual = List.of("B", "A", "C");

        assertEquals(Set.of("A", "B", "C"), Set.copyOf(actual));
    }
}

Be careful: converting to a set removes duplicates.

If duplicates matter but order does not, sort both lists first:

import org.junit.jupiter.api.Test;

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

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

class SortedListTest {

    @Test
    void shouldHaveSameItemsIgnoringOrderButKeepingDuplicates() {
        List<String> actual = new ArrayList<>(List.of("B", "A", "A"));
        List<String> expected = new ArrayList<>(List.of("A", "A", "B"));

        actual.sort(Comparator.naturalOrder());
        expected.sort(Comparator.naturalOrder());

        assertEquals(expected, actual);
    }
}

9. Testing a Mutable Collection

import org.junit.jupiter.api.Test;

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

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

class MutableListTest {

    @Test
    void shouldAddItemToList() {
        List<String> items = new ArrayList<>();

        items.add("Book");

        assertEquals(1, items.size());
        assertTrue(items.contains("Book"));
    }
}

10. Testing Exceptions for Immutable Collections

Collections created with List.of, Set.of, or Map.of are immutable.

import org.junit.jupiter.api.Test;

import java.util.List;

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

class ImmutableCollectionTest {

    @Test
    void shouldThrowWhenModifyingImmutableList() {
        List<String> items = List.of("A", "B");

        assertThrows(UnsupportedOperationException.class, () -> {
            items.add("C");
        });
    }
}

11. Testing with AssertJ

If your project uses AssertJ, collection assertions are often more readable.

Maven dependency:

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

Example:

import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Map;
import java.util.Set;

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

class AssertJCollectionTest {

    @Test
    void shouldTestCollectionsFluently() {
        List<String> names = List.of("Alice", "Bob");

        assertThat(names)
                .hasSize(2)
                .contains("Alice")
                .containsExactly("Alice", "Bob");

        Set<String> roles = Set.of("ADMIN", "USER");

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

        Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 88);

        assertThat(scores)
                .hasSize(2)
                .containsEntry("Alice", 95)
                .containsKey("Bob");
    }
}

AssertJ is especially useful for:

assertThat(list).containsExactly("A", "B");
assertThat(list).containsExactlyInAnyOrder("B", "A");
assertThat(set).contains("A");
assertThat(map).containsEntry("key", "value");
assertThat(collection).isEmpty();

Quick Reference

What you want to test JUnit assertion
List equals expected order assertEquals(expectedList, actualList)
Iterable equals expected order assertIterableEquals(expected, actual)
Set contains same items assertEquals(expectedSet, actualSet)
Map contains same entries assertEquals(expectedMap, actualMap)
Collection has size assertEquals(expectedSize, collection.size())
Collection is empty assertTrue(collection.isEmpty())
Collection contains item assertTrue(collection.contains(item))
Collection does not contain item assertFalse(collection.contains(item))

Recommended Style

For standard JUnit 5 tests:

assertEquals(List.of("A", "B"), actualList);
assertEquals(Set.of("A", "B"), actualSet);
assertEquals(Map.of("A", 1, "B", 2), actualMap);
assertTrue(actualList.contains("A"));
assertTrue(actualList.isEmpty());

For more readable collection tests, use AssertJ:

assertThat(actualList).containsExactly("A", "B");
assertThat(actualList).containsExactlyInAnyOrder("B", "A");
assertThat(actualMap).containsEntry("A", 1);

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.