This content originally appeared on DEV Community and was authored by M. Oly Mahmud
Testing is very important to develop a good software. In this guide. We’ll see how to perform unit testing in a Spring Boot app. I’ll try to make it as simple as possible.
1. What is Unit Testing?
Unit testing is the process of testing individual parts (units) of your application. In unit testing, we test the functions or methods, in isolation.
The goal is to make sure each unit works as expected.
Examples of unit testing are,
- Testing if a method correctly saves a todo item.
- Testing if fetching by ID returns the correct item.
- Testing if delete operation calls the repository properly.
2. Project Overview
Here’s the structure of our Spring Boot project:
src/main/
├── java/com/example/unittesting
│ ├── controller/TodoController.java
│ ├── entity/Todo.java
│ ├── repository/TodoRepository.java
│ ├── service/TodoService.java
│ ├── service/impl/TodoServiceImpl.java
│ └── UnitTestingApplication.java
└── resources/
└── application.properties
src/test/
└── java/com/example/unittesting
└── service/TodoServiceImplTest.java
We’ll focus on the service layer tests because they are perfect for unit testing with Mockito (no need to connect to the real database).
3. Entity Layer
The Todo entity represents a single task.
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private boolean completed;
}
This is a simple JPA entity with id, title, and completed fields.
4. Repository Layer
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
This interface extends JpaRepository, giving you built-in CRUD operations.
We won’t test this layer directly because Spring Data JPA already provides these methods. Instead, we’ll mock it in our service tests.
5. Service Layer
The TodoService defines our business operations:
public interface TodoService {
List<Todo> getAllTodos();
Todo getTodoById(Long id);
Todo createTodo(Todo todo);
Todo updateTodo(Long id, Todo todo);
void deleteTodo(Long id);
}
The implementation:
@Service
public class TodoServiceImpl implements TodoService {
private final TodoRepository todoRepository;
public TodoServiceImpl(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
@Override
public List<Todo> getAllTodos() {
return todoRepository.findAll();
}
@Override
public Todo getTodoById(Long id) {
return todoRepository.findById(id).orElse(null);
}
@Override
public Todo createTodo(Todo todo) {
return todoRepository.save(todo);
}
@Override
public Todo updateTodo(Long id, Todo todo) {
return todoRepository.save(todo);
}
@Override
public void deleteTodo(Long id) {
todoRepository.deleteById(id);
}
}
This layer is perfect for unit testing because it calls the repository and contains logic we can verify.
6. Writing Unit Tests with Mockito and JUnit 5
Our test class:
src/test/java/com/example/unittesting/service/TodoServiceImplTest.java
@ExtendWith(MockitoExtension.class)
class TodoServiceImplTest {
// Mock the repository to avoid using a real database
@Mock
private TodoRepository todoRepository;
// Inject the mock into our service implementation
@InjectMocks
private TodoServiceImpl todoServiceImpl;
// ---------- Test 1 ----------
@Test
void createTodo_shouldSaveAndReturnTodo() {
// Arrange: create a sample Todo and mock repository behavior
Todo todo = new Todo(1L, "Buy groceries", false);
when(todoRepository.save(todo)).thenReturn(todo);
// Act: call the method
Todo created = todoServiceImpl.createTodo(todo);
// Assert: check if returned object is correct
assertNotNull(created);
assertEquals(1L, created.getId());
assertEquals("Buy groceries", created.getTitle());
assertFalse(created.isCompleted());
verify(todoRepository, times(1)).save(todo); // verify repository interaction
}
// ---------- Test 2 ----------
@Test
void getAllTodos_shouldReturnAllTodos() {
// Arrange
Todo todo1 = new Todo(1L, "Buy groceries", false);
Todo todo2 = new Todo(2L, "Cook dinner", true);
when(todoRepository.findAll()).thenReturn(List.of(todo1, todo2));
// Act
List<Todo> todos = todoServiceImpl.getAllTodos();
// Assert
assertEquals(2, todos.size());
verify(todoRepository, times(1)).findAll();
}
// ---------- Test 3 ----------
@Test
void getTodoById_shouldReturnTodoIfExists() {
// Arrange
Todo todo = new Todo(1L, "Buy groceries", false);
when(todoRepository.findById(1L)).thenReturn(Optional.of(todo));
// Act
Todo found = todoServiceImpl.getTodoById(1L);
// Assert
assertNotNull(found);
assertEquals(1L, found.getId());
verify(todoRepository, times(1)).findById(1L);
}
// ---------- Test 4 ----------
@Test
void updateTodo_shouldUpdateAndReturnUpdatedTodo() {
// Arrange
Todo existing = new Todo(1L, "Buy groceries", false);
Todo updated = new Todo(1L, "Buy groceries and cook dinner", true);
when(todoRepository.save(existing)).thenReturn(updated);
// Act
Todo result = todoServiceImpl.updateTodo(1L, existing);
// Assert
assertNotNull(result);
assertEquals("Buy groceries and cook dinner", result.getTitle());
assertTrue(result.isCompleted());
verify(todoRepository, times(1)).save(existing);
}
// ---------- Test 5 ----------
@Test
void deleteTodo_shouldCallRepositoryDeleteById() {
// Arrange
doNothing().when(todoRepository).deleteById(1L);
// Act
todoServiceImpl.deleteTodo(1L);
// Assert
verify(todoRepository, times(1)).deleteById(1L);
}
}
Key Points:
-
@ExtendWith(MockitoExtension.class)integrates Mockito with JUnit 5. -
@Mockcreates a mock version ofTodoRepository. -
@InjectMocksinjects the mock into the service class. -
when(...).thenReturn(...)defines mock behavior. -
verify(...)ensures the repository methods were called as expected.
7. Test Configuration
We’re using an in-memory H2 database, though we don’t really hit it during unit testing since we mock the repository.
src/main/resources/application.properties
spring.application.name=unit-testing
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=sa
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
This setup helps when you run integration tests later.
8. Running the Tests
If you’re using IntelliJ or VS Code, you can simply right-click the test class and choose Run Tests.
Or run it from the terminal:
./mvnw test
You’ll see all test cases executed successfully:
BUILD SUCCESS
9. Summary
In this guide, we’ve seen how to:
- Structure a Spring Boot project for testing
- Use Mockito to mock dependencies
- Write unit tests for CRUD operations in the service layer
- Verify repository interactions
This content originally appeared on DEV Community and was authored by M. Oly Mahmud