DEV Community

Cover image for Unit Testing Using Spring Boot, JUnit and Mockito
M. Oly Mahmud
M. Oly Mahmud

Posted on

Unit Testing Using Spring Boot, JUnit and Mockito

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

This is a simple JPA entity with id, title, and completed fields.

4. Repository Layer

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • @ExtendWith(MockitoExtension.class) integrates Mockito with JUnit 5.
  • @Mock creates a mock version of TodoRepository.
  • @InjectMocks injects 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You’ll see all test cases executed successfully:

BUILD SUCCESS
Enter fullscreen mode Exit fullscreen mode

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)