Юнит-тесты

Юнит-тест представляет собой наиболее узконаправленный вариант автоматизированного теста.

Термин "юнит-тест" используется для описания различных концепций, в том числе для обозначения автоматизированного тестирования в целом. Мы будем относиться к юнит-тесту как к автоматизированному тесту, проверяющему поведение определенного класса или набора классов без зависимостей (в первую очередь без контекста Spring и базы данных).

Jmix автоматически включает фреймворк тестирования JUnit 5, а также Mockito для поддержки создания фиктивных объектов (мокинга) зависимостей.

Тестирование изолированной функциональности

Для демонстрации процесса создания юнит-теста рассмотрим функциональность вычисления общей суммы для списка экземпляров OrderLine, связанных с Order.

Это вычисление выполняет отдельный класс под названием OrderAmountCalculation. Это не бин Spring, а обычный класс Java:

public class OrderAmountCalculation {

    public BigDecimal calculateTotalAmount(List<OrderLine> orderLines) {

        return orderLines.stream()
                .map(this::totalPriceForOrderLine)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal totalPriceForOrderLine(OrderLine orderLine) {

        BigDecimal productPrice = orderLine.getProduct().getPrice();
        BigDecimal quantity = BigDecimal.valueOf(orderLine.getQuantity());

        return productPrice.multiply(quantity);
    }
}

Пример юнит-теста для этой функциональности:

class OrderAmountCalculationTest {

    private OrderAmountCalculation orderAmountCalculation;
    private static final BigDecimal USD499 = BigDecimal.valueOf(499.0);
    private Product iPad;

    @Test
    void calculateTotalAmount() {

        // given:
        orderAmountCalculation = new OrderAmountCalculation(); (1)

        // and:
        iPad = new Product(); (2)
        iPad.setName("Apple iPad");
        iPad.setPrice(USD499);

        // and:
        OrderLine twoIpads = new OrderLine();
        twoIpads.setProduct(iPad);
        twoIpads.setQuantity(2.0);

        // when:
        var totalAmount = orderAmountCalculation.calculateTotalAmount(
                List.of(twoIpads)
        );

        // then:
        assertThat(totalAmount) (3)
                .isEqualByComparingTo(BigDecimal.valueOf(998.0));
    }
}
1 Класс OrderAmountCalculation создается через конструктор без использования Spring.
2 Сущности создаются путем вызова конструктора (без использования API Jmix Metadata).
3 Проверка результата вычислений выполняется с использованием утверждений AssertJ.

Данный класс теста не содержит аннотаций тестов Spring Boot (например, @SpringBootTest), поэтому тест не использует контекст Spring и, следовательно, выполняется очень быстро. Однако отсутствие контекста Spring в тесте также означает, что невозможно использовать @Autowired в классе теста для получения экземпляров бинов Spring. Если у тестируемого класса есть зависимости от бинов Spring, эти зависимости должны быть созданы вручную.

Создание фиктивных объектов с Mockito

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

Рассмотрим следующий пример: есть класс, который вызывает API Jmix TimeSource для получения текущей даты. Он используется для подсчета количества бронирований, размещенных в текущем году для определенного клиента.

Вот реализация этого класса:

@Component
public class RecentOrdersCounter {
    private final TimeSource timeSource;

    public RecentOrdersCounter(TimeSource timeSource) {
        this.timeSource = timeSource;
    }

    public long countFromThisYear(Customer customer) {
        return customer.getOrders().stream()
                .filter(this::fromThisYear)
                .count();
    }

    private boolean fromThisYear(Order order) {
        int thisYear = timeSource.now().toLocalDate().getYear();
        return thisYear == order.getDate().getYear();
    }
}

Класс аннотирован как @Component, чтобы Spring автоматически создавал его и внедрял зависимости. Но если вы хотите протестировать эту функциональность в юнит-тесте, вам нужно вручную создать экземпляр класса RecentOrdersCounter и предоставить ему экземпляр TimeSource через конструктор.

Для тестирования функциональности RecentOrdersCounter имеет смысл проверить следующее:

Предположим, у нас есть два заказа: один из 2019 года и один из 2020 года. Когда текущий год - 2020, мы ожидаем получить счетчик равным единице.

Для достижения этого необходимо управлять тем, что возвращает TimeSource как текущее время, в частности, эмулировать тот факт, что текущий год - 2020.

Такую эмуляцию можно реализовать с помощью Mockito - библиотеки для создания фиктивных объектов (моков). Она доступна в проектах Jmix по умолчанию.

Вот пример того, как может выглядеть тест:

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class RecentOrdersCounterTest {

    TimeSource timeSourceMock = mock(TimeSource.class); (1)

    LocalDateTime MAR_01_2020 = LocalDate.of(2020, 3, 1).atStartOfDay();

    @Test
    void given_itIs2020_and_customerWithOneOrderIn2020_when_countFromThisYear_then_resultIs1() {

        // given:
        when(timeSourceMock.now())
                .thenReturn(ZonedDateTime.of(MAR_01_2020, ZoneId.systemDefault()));  (2)

        // and:
        RecentOrdersCounter counter = new RecentOrdersCounter(timeSourceMock); (3)

        // and:
        Customer customer = new Customer();
        Order orderFrom2020 = orderWithDate(LocalDate.of(2020, 2, 5));
        Order orderFrom2019 = orderWithDate(LocalDate.of(2019, 5, 1));
        customer.setOrders(List.of(orderFrom2020, orderFrom2019));

        // when:
        long recentOrdersCount = counter.countFromThisYear(customer);

        // then:
        assertThat(recentOrdersCount)
                .isEqualTo(1);
    }
1 Метод Mockito.mock() создает фиктивный экземпляр, который можно использовать для управления поведением класса.
2 Вызов Mockito.when() определяет, что при вызове метода now() на TimeSource он должен возвращать 2020-03-01 в виде ZonedDateTime.
3 При создании экземпляра класса счетчика в конструктор передается мок (фиктивный экземпляр) TimeSource.
Если вы собираетесь тестировать ваши Spring-компоненты в юнит-тестах, используйте инжекцию зависимостей через конструктор вместо @Autowired на полях класса.

Более подробную информацию о использовании Mockito можно найти в его документации.

Проверка поведения с использованием утверждений

Утверждения могут быть выражены с использованием библиотеки AssertJ.

DSL AssertJ предоставляет fluent API для выполнения проверок результатов тестируемых классов. Методы утверждений (например, assertThat) должны быть статически импортированы из org.assertj.core.api.Assertions, например:

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

Вот простой пример утверждения AssertJ для строки:

// given:
String customerName = "Mike Myers";

// expect:
assertThat(customerName)
        .startsWith("Mike")
        .endsWith("Myers");

Обратите внимание, что можно объединить несколько утверждений, принадлежащих одному объекту результата.

В случае неудачного теста JUnit / AssertJ предоставит правильное сообщение об ошибке с разницей между ожидаемым и фактическим поведением:

Expecting actual:
  "Mike Myers"
to end with:
  "Murphy"

В зависимости от типа объекта AssertJ предоставляет различные методы утверждений для сравнения значений. Например, при сравнении списков AssertJ предоставляет методы hasSize и contains:

// given:
String bruceWillis = "Bruce Willis";
String mikeMyers = "Mike Myers";
String eddiMurphy = "Eddi Murphy";

// when:
List<String> customers = List.of(mikeMyers, eddiMurphy);

// expect:
assertThat(customers)
        .hasSize(2)
        .contains(eddiMurphy)
        .doesNotContain(bruceWillis);

Дополнительную информацию о методах утверждений см. в документации AssertJ.