В этой статье мы увидим JUnit и Mockito в действии с использованием двух компонентов Java, класса службы и класса репозитория.

Мы начнем с создания классов, а затем напишем тесты по-разному, используя такие понятия, как утверждение, проверка, проверка на выброшенное исключение, ArgumentMatcher и ArgumentCaptor. Наконец, мы создадим более чистые тесты, извлекая дублированный код и даже используя аннотации Mockito. Мы не будем фокусироваться на 100% покрытии кода.

Тестируемый код

DataRepository.java

public interface DataRepository {
    int[] retrieveAllData();
    int getStoredSumById(int id);
    void save(Object o);
}

DataService.java

public interface DataService {
    int calculateSum();
    void setDataRepository(DataRepository dataRepository);
    int calculateNewSum(int id);
    void save(Data o);
}

DataServiceImpl.java

public class DataServiceImpl implements DataService {

    private DataRepository dataRepository;

    public void setDataRepository(DataRepository dataRepository) {
        this.dataRepository = dataRepository;
    }

    public int calculateSum(){
        int sum = 0;
        for(int value : dataRepository.retrieveAllData()){
            sum += value;
        }
        return sum;
    }

    public int calculateNewSum(int id){
        int sum = dataRepository.getStoredSumById(id);
        return sum + sum;
    }

    public void save(Data o){
        o = new Data(o.getName().toUpperCase());
        dataRepository.save(o);
    }
}

Данные.java

public class Data {
    private String name;
    // constructors, getters and setters
}

Модульные тесты без аннотаций Mockito

Тест для метода calculateSum() из класса обслуживания, фиктивный репозиторий. В этом тесте мы предполагаем, что фиктивный метод retrieveAllData() из репозитория возвращает массив с данными.

@Test
public void calculateSum_Should_ReturnResult_When_DataIsProvided() {
 //create service under test
 DataService ms = new DataServiceImpl();

 //mock repository to test service in isolation
 DataRepository dataRepositoryMock = mock(DataRepository.class);
 when(dataRepositoryMock.retrieveAllData())
                                .thenReturn(new int[]{1, 2, 3});

 //set mock to service
 ms.setDataRepository(dataRepositoryMock);

 //call method under test
 int result = ms.calculateSum();

 //verify if method on the mock is called by service under test
 //it is mostly used when a method that is called on a mock 
 //does not have a return
 verify(dataRepositoryMock, times(1)).retrieveAllData();

 //assert result
 assertEquals(6, result);
}

Тест для метода calculateSum() из класса обслуживания, фиктивный репозиторий. Мы предполагаем, что фиктивный метод retrieveAllData() из репозитория возвращает массив без данных.

@Test
public void calculateSum_Should_ReturnZero_When_DataIsEmpty() {
    //create service under test
    DataService ms = new DataServiceImpl();

    //mock repository to test service in isolation
    DataRepository dataRepositoryMock = mock(DataRepository.class);
    when(dataRepositoryMock.retrieveAllData())
                                          .thenReturn(new int[]{});

    //set mock to service
    ms.setDataRepository(dataRepositoryMock);

    //call method under test
    int result = ms.calculateSum();

    //verify if method on the mock is called by service under test
    verify(dataRepositoryMock, times(1)).retrieveAllData();

    //assert result
    assertEquals(0, result);
}

Тест для метода calculateSum() из класса обслуживания, фиктивный репозиторий. Предполагая, что имитированный метод retrieveAllData() из репозитория возвращает значение null, в результате чего метод calculateSum() из класса обслуживания выдает исключение NullPointerException.

@Test
public void calculateSum_Should_ThrowException_When_DataIsNull() {
    assertThrows(NullPointerException.class, () -> {
        //create service under test
        DataService ms = new DataServiceImpl();

        //mock repository to test service in isolation
        DataRepository dataRepositoryMock = 
           mock(DataRepository.class);
        when(dataRepositoryMock.retrieveAllData()).thenReturn(null);

        //set mock to service
        ms.setDataRepository(dataRepositoryMock);

        //call method under test
        ms.calculateSum();
    });
}

Протестируйте метод calculateNewSum() из класса обслуживания, имитируя репозиторий с помощью ArgumentMatchers. В этом тесте мы предполагаем, что фиктивный метод getStoredSumById(‹вызывается с любым Integer›) из репозитория возвращает 2. Новая тема, представленная здесь, — это ArgumentMatchers, такие как any(), anyInt() и т. д., которые можно использовать для замены фактический аргумент, чтобы сделать тесты более общими.

@Test
void calculateNewSum_Should_ReturnResult_When_DataIsProvided() {
    //create service under test
    DataService ms = new DataServiceImpl();

    //mock repository to test service in isolation
    DataRepository dataRepositoryMock = mock(DataRepository.class);

    //return 2 when method is called with any int value
    when(dataRepositoryMock.getStoredSumById(anyInt()))
                                                    .thenReturn(2);

    //set mock to service
    ms.setDataRepository(dataRepositoryMock);

    //call method under test
    int result = ms.calculateNewSum(1);

    //verify if method on the mock is called by 
    //service under test with any argument
    verify(dataRepositoryMock, times(1)).getStoredSumById(anyInt());

    //assert result
    assertEquals(4, result);
}

Тест на метод save() из класса обслуживания, имитирующий репозиторий. В этом случае метод save() из класса сервиса ничего не возвращает для подтверждения. Мы можем использовать ArgumentCaptor, чтобы проверить, вызывается ли метод save() из класса репозитория с ожидаемыми аргументами.

@Test
void save_ShouldCallRepository_With_GivenParam() {
    // create service under test
    DataService ms = new DataServiceImpl();

    // mock repository to test service in isolation
    DataRepository dataRepositoryMock = mock(DataRepository.class);

    // set mock to service
    ms.setDataRepository(dataRepositoryMock);

    // call method under test
    Data o = new Data("MockitoObject");
    ms.save(o);

    //create expected object
    Data expected = new Data("MOCKITOOBJECT");

    // because the method does not return anything we can check
    // if mock method was called with an expected parameter
    ArgumentCaptor<Data> captor = 
                            ArgumentCaptor.forClass(Data.class);
    verify(dataRepositoryMock, times(1)).save(captor.capture());

    //assert captured argument
    assertEquals(expected, captor.getValue());
}

Более чистые модульные тесты без аннотаций Mockito

Мы могли бы извлечь повторяющийся код из каждого теста во внешние методы, как в примере ниже, чтобы сделать наш код чище.

public class DataServiceTest_Clean {
    //create service under test
    DataService ms = new DataServiceImpl();

    //mock repository to test service in isolation
    DataRepository dataRepositoryMock = mock(DataRepository.class);

    @BeforeEach
    public void beforeEach(){
        //set mock to service
        ms.setDataRepository(dataRepositoryMock);
    }

    @Test
    public void calculateSum_Should_ReturnResult_When_Data() {
        //mock repository to test service in isolation
        when(dataRepositoryMock.retrieveAllData())
                                .thenReturn(new int[]{1, 2, 3});

        //call method under test
        int result = ms.calculateSum();

        //verify if method on the mock is called by 
        //service under test
        //it is mostly used when a method that is 
        //called on a mock does not have a return
        verify(dataRepositoryMock, times(1)).retrieveAllData();

        //assert result
        assertEquals(6, result);
    }
}

Модульные тесты с аннотациями Mockito

В приведенном выше примере мы продемонстрировали, как мы можем удалить дублированный код из нашего теста, но мы можем сделать шаг вперед и использовать аннотации Mockito для создания макетов и внедрения их с помощью аннотаций.

@ExtendWith(MockitoExtension.class)
public class DataServiceTest_Clean_WithAnnotations {
    //create service under test and inject all mocks needed
    //there is no need to manually inject 
    //mockitoRepositoryMock, just create it with @Mock
    @InjectMocks
    DataServiceImpl ms;

    //mock repository to test service in isolation
    @Mock
    DataRepository dataRepositoryMock;

    @Test
    void calculateSum_Should_ReturnResult_When_DataIsProvided() {
        //mock repository to test service in isolation
        when(dataRepositoryMock.retrieveAllData())
                                 .thenReturn(new int[]{1, 2, 3});

        //call method under test
        int result = ms.calculateSum();

        //verify if method on the mock is called 
        //by service under test
        //it is mostly used when a method that is called 
        //on a mock does not have a return
        verify(dataRepositoryMock, times(1)).retrieveAllData();

        //assert result
        assertEquals(6, result);
    }
}

Код доступен через GitHub.

Подробнее о тестировании

Для более сложного примера вы можете проверить этот проект на GitHub.

Первоначально опубликовано на https://weinspire.tech 10 марта 2020 г.