Exercise 2: Write Nested Tests for TodoItemService Class

This exercise helps you to understand how you can use nested test classes in real software projects. During this exercise you will write tests for the findById() method of the TodoItemService class. You must verify that the implementation of this method fulfills these requirements:

  • If the requested todo item is found, this method must return a TodoItemDTO object that contains the information of the found todo item.
  • If the requested todo item isn’t found, this method must throw a NotFoundException.

The source code of the TodoItemService class looks as follows:

public class TodoItemService {

    private final TodoItemRepository repository;

    public TodoItemService(TodoItemRepository repository) {
        this.repository = repository;
    }

    public TodoItemDTO findById(Long id) {
        return repository.findById(id)
                .map(source -> {
                    var todoItem = new TodoItemDTO();

                    todoItem.setId(source.getId());
                    todoItem.setResolution(source.getResolution());
                    todoItem.setStatus(source.getStatus());
                    todoItem.setTitle(source.getTitle());

                    return todoItem;
                })
                .orElseThrow(
                	() -> new NotFoundException("Todo item was not found")
                );
    }
}

The TodoItemRepository interface declares one method called findById() which takes the id of the requested todo item as a method parameter. If the requested todo item is found from the database, this method returns an Optional object that contains the found todo item. On the other hand, if the requested todo item isn’t found from the database, this method returns an empty Optional object.

The source code of the TodoItemRepository interface looks as follows:

import java.util.Optional;

interface TodoItemRepository {

    Optional<TodoItem> findById(Long id);
}

The TodoItem class is a model object that cannot be visible to the outside world. It contains the information of one todo item. The source code of the TodoItem class looks as follows:

class TodoItem {

    private Long id;
    private TodoItemResolution resolution;
    private TodoItemStatus status;
    private String title;

    //Getter and setters are omitted
}

The TodoItemDTO class is a DTO class that contains the information of one todo item. Its source code looks as follows:

public class TodoItemDTO {

    private Long id;
    private TodoItemResolution resolution;
    private TodoItemStatus status;
    private String title;

    //Getters and setters are omitted
}

The TodoItemResolution enum specifies the resolutions of a todo item. Its source code looks as follows:

public enum TodoItemResolution {

    DONE,
    DUPLICATE,
    WONT_DO
}

The TodoItemStatus enum specifies the statuses of a todo item. Its source code looks as follows:

public enum TodoItemStatus {

    CLOSED,
    IN_PROGRESS,
    OPEN
}
When you write the required tests, you have to use libraries which might not be familiar to you (AssertJ and Mockito). Don’t worry. I will guide you through this exercise.

You can finish this exercise by following these steps:

1. Create a new test class (TodoItemServiceTest) and add this class to the com.cleantestautomation.junit5intro package. Remember to give a proper display name to the created class (‘Tests for read operations of todo items’).

2. Add these fields to the TodoItemServiceTest class:

private TodoItemRepository repository;
private TodoItemService service;

3. Add a new setup method to your test class. This method must be invoked before a test method is run. After you have added this method to your test class, you must implement it by using this code:

repository = mock(TodoItemRepository.class);
service = new TodoItemService(repository);

Remember to add this static import to your test class or your code won’t compile:

import static org.mockito.Mockito.mock;

This setup method replaces the real TodoItemRepository object with a mock object that’s created by Mockito and uses the created mock object when it creates a new TodoItemService object. After this code is run, you can specify the object that’s returned by the findById() method of the TodoItemRepository interface.

4. Add a new nested test class to the TodoItemServiceTest class (FindById) and specify its display name (‘Find todo item by using its id as search criteria’). This class contains the test methods which ensure that the system under test (TodoItemService) is working as expected.

5. Add the following constant to the FindById class:

private final Long ID = 2L;
You should add this constant to the FindById class because it’s used by the test methods found from two different nested test classes.

6. Add a new nested test class (WhenTodoItemIsNotFound) to the FindById class and specify its display name (‘When the todo item isn’t found’). This class contains the test method which ensures that the system under test (TodoItemService) is working as expected when the requested todo item isn’t found.

7. Add a setup method to the WhenTodoItemIsNotFound class. When you implement this method, you must ensure that the TodoItemRepository stub returns an empty Optional object when its findById() method is invoked by using the argument: 2L. You can implement your setup method by using this code:

given(repository.findById(ID)).willReturn(Optional.empty());

Remember to add this static import to your test class or your code won’t compile:

import static org.mockito.BDDMockito.given;

8. Add a new test method (shouldThrowException()) to the WhenTodoItemIsNotFound class and specify its display name(‘Should throw an exception’). This test verifies that the system under test (TodoItemService) throws a NotFoundException when its findById() method is invoked by using the argument: 2L. You can implement your test method by using this code:

assertThatThrownBy(() -> service.findById(ID))
         .isExactlyInstanceOf(NotFoundException.class);

Remember to add this static import to your test class or your code won’t compile:

import static org.assertj.core.api.Assertions.assertThatThrownBy;

9. Add a new nested test class (WhenTodoItemIsFound) to the FindById class and specify its display name (‘When the todo item is found’). This class contains the test method which ensures that the system under test (TodoItemService) is working as expected when the requested todo item is found.

10. Add these constants to the WhenTodoItemIsFound class:

private final TodoItemResolution RESOLUTION = TodoItemResolution.DUPLICATE;
private final TodoItemStatus STATUS = TodoItemStatus.CLOSED;
private final String TITLE = "Write an exercise";
You should add these constants to the WhenTodoItemIsFound class because they are used by the test method found from the WhenTodoItemIsFound class.

11. Add a setup method to the WhenTodoItemIsFound class. When you implement this method, you must ensure that the TodoItemRepository stub returns an Optional object that contains the information of the found todo item when its findById() method is invoked by using the argument: 2L. You can implement your setup method by using this code:

var todoItem = new TodoItem();
todoItem.setId(ID);
todoItem.setResolution(RESOLUTION);
todoItem.setStatus(STATUS);
todoItem.setTitle(TITLE);

given(repository.findById(ID)).willReturn(Optional.of(todoItem));

12. Add a new test method (shouldReturnInformationOfFoundTodoItem()) to the WhenTodoItemIsFound class and specify its display name (‘Should return the information of the found todo item’). This test verifies that the system under test (TodoItemService) returns the information of the found todo item when its findById() method is invoked by using the argument: 2L. You can implement your test method by using this code:

var found = service.findById(ID);

assertSoftly(softAssertions -> {
    softAssertions.assertThat(found.getId())
            .as("id")
            .isEqualByComparingTo(ID);
    softAssertions.assertThat(found.getResolution())
            .as("resolution")
            .isEqualTo(RESOLUTION);
    softAssertions.assertThat(found.getStatus())
            .as("status")
            .isEqualTo(STATUS);
    softAssertions.assertThat(found.getTitle())
            .as("title")
            .isEqualTo(TITLE);
});

Remember to add this static import to your test class or your code won’t compile:

import static org.assertj.core.api.SoftAssertions.assertSoftly;

13. Run your tests and makes sure that they pass. Also, take a look at the test report that’s displayed by your IDE. It should demonstrate why it’s important to specify proper display names for test classes, nested test classes, and test methods.