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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.