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:
- Add
junit-jupiterandmockito-junit-jupiter. - Annotate the test with
@ExtendWith(MockitoExtension.class). - Mock dependencies with
@Mock. - Create the service with
@InjectMocks. - Stub dependency behavior with
when(...).thenReturn(...). - Call the service method.
- Assert the result with JUnit assertions.
- Verify interactions with Mockito when useful.
For most service classes, this gives you fast, focused, and reliable unit tests.
