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
 
 
              
 
    
Top comments (0)