This comprehensive guide will walk you through creating a Spring Boot application with repository and service layers, then writing unit tests using Mockito's @Mock
and @InjectMocks
annotations.
1. Project Setup
First, create a Spring Boot project with these dependencies (in your pom.xml
or build.gradle
):
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database (for testing) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok (optional but recommended) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. Entity Class
Create a simple entity for our example:
package com.example.demo.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private Integer age;
}
3. Repository Layer
Create a JPA repository interface:
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByAgeGreaterThan(int age);
boolean existsByEmail(String email);
}
4. Service Layer
Create a service class that will use the repository:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.exception.UserAlreadyExistsException;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
if (userRepository.existsByEmail(user.getEmail())) {
throw new UserAlreadyExistsException("Email already in use: " + user.getEmail());
}
return userRepository.save(user);
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + id));
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public List<User> getUsersOlderThan(int age) {
return userRepository.findByAgeGreaterThan(age);
}
public User updateUser(Long id, User userDetails) {
User user = getUserById(id);
user.setName(userDetails.getName());
user.setEmail(userDetails.getEmail());
user.setAge(userDetails.getAge());
return userRepository.save(user);
}
public void deleteUser(Long id) {
User user = getUserById(id);
userRepository.delete(user);
}
}
5. Exception Classes
Create custom exceptions for error handling:
package com.example.demo.exception;
public class UserAlreadyExistsException extends RuntimeException {
public UserAlreadyExistsException(String message) {
super(message);
}
}
package com.example.demo.exception;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
6. Unit Testing with Mockito
Now, let's create unit tests for the UserService
using Mockito's @Mock
and @InjectMocks
:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.exception.UserAlreadyExistsException;
import com.example.demo.exception.UserNotFoundException;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
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.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
private User user1;
private User user2;
@BeforeEach
void setUp() {
user1 = new User(1L, "John Doe", "john@example.com", 30);
user2 = new User(2L, "Jane Smith", "jane@example.com", 25);
}
@Test
void createUser_Success() {
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(user1);
User createdUser = userService.createUser(user1);
assertNotNull(createdUser);
assertEquals("John Doe", createdUser.getName());
verify(userRepository, times(1)).existsByEmail(user1.getEmail());
verify(userRepository, times(1)).save(user1);
}
@Test
void createUser_ThrowsWhenEmailExists() {
when(userRepository.existsByEmail(anyString())).thenReturn(true);
assertThrows(UserAlreadyExistsException.class, () -> {
userService.createUser(user1);
});
verify(userRepository, times(1)).existsByEmail(user1.getEmail());
verify(userRepository, never()).save(any(User.class));
}
@Test
void getUserById_Success() {
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user1));
User foundUser = userService.getUserById(1L);
assertNotNull(foundUser);
assertEquals("John Doe", foundUser.getName());
verify(userRepository, times(1)).findById(1L);
}
@Test
void getUserById_ThrowsWhenNotFound() {
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
assertThrows(UserNotFoundException.class, () -> {
userService.getUserById(1L);
});
verify(userRepository, times(1)).findById(1L);
}
@Test
void getAllUsers_Success() {
when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));
List<User> users = userService.getAllUsers();
assertEquals(2, users.size());
verify(userRepository, times(1)).findAll();
}
@Test
void getUsersOlderThan_Success() {
when(userRepository.findByAgeGreaterThan(anyInt())).thenReturn(Arrays.asList(user1));
List<User> users = userService.getUsersOlderThan(25);
assertEquals(1, users.size());
assertEquals("John Doe", users.get(0).getName());
verify(userRepository, times(1)).findByAgeGreaterThan(25);
}
@Test
void updateUser_Success() {
User updatedDetails = new User(null, "John Updated", "john.updated@example.com", 31);
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user1));
when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));
User updatedUser = userService.updateUser(1L, updatedDetails);
assertNotNull(updatedUser);
assertEquals("John Updated", updatedUser.getName());
assertEquals("john.updated@example.com", updatedUser.getEmail());
assertEquals(31, updatedUser.getAge());
verify(userRepository, times(1)).findById(1L);
verify(userRepository, times(1)).save(user1);
}
@Test
void deleteUser_Success() {
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user1));
doNothing().when(userRepository).delete(any(User.class));
userService.deleteUser(1L);
verify(userRepository, times(1)).findById(1L);
verify(userRepository, times(1)).delete(user1);
}
}
Key Concepts Explained
@Mock
Annotation
- Creates a mock instance of the class/interface
- Used to mock dependencies of the class under test
- All methods of a mock return default values unless specifically stubbed
@InjectMocks
Annotation
- Creates an instance of the class under test
- Automatically injects the mock dependencies (marked with
@Mock
) into it - Used for the class you're actually testing
Mockito Methods Used:
when().thenReturn()
- Stubs a method to return a specific valueverify()
- Verifies that a method was called with specific parametersany()
,anyLong()
,anyString()
- Argument matchers for flexible verificationtimes()
,never()
- Verification modes for method callsdoNothing().when()
- For void methods that shouldn't do anything
Testing Best Practices:
- Each test should focus on a single scenario
- Follow the Arrange-Act-Assert pattern
- Verify both the return values and the interactions with mocks
- Test both happy paths and error cases
- Keep tests independent of each other
This guide provides a complete example of how to structure a Spring Boot application with repository and service layers, and how to effectively test the service layer using Mockito's mocking capabilities.