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, […]
Artikel oleh Wayan · Sumber: Kode Java ↗
Terjemahan artikel ini belum tersedia. Isi asli ditampilkan.
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 = "alice@example.com";
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("alice@example.com", 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 = "alice@example.com";
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", "alice@example.com");
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("alice@example.com")).thenReturn(true);
when(userRepository.existsByEmail("alice@example.com")).thenReturn(false);
when(userRepository.save(any(User.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
// Act
userService.createUser("Alice", "alice@example.com");
// Assert
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("Alice", capturedUser.name());
assertEquals("alice@example.com", 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.
