Юнит-тесты
Юнит-тест представляет собой наиболее узконаправленный вариант автоматизированного теста.
Термин "юнит-тест" используется для описания различных концепций, в том числе для обозначения автоматизированного тестирования в целом. Мы будем относиться к юнит-тесту как к автоматизированному тесту, проверяющему поведение определенного класса или набора классов без зависимостей (в первую очередь без контекста Spring и базы данных).
Тестирование изолированной функциональности
Для демонстрации процесса создания юнит-теста рассмотрим функциональность вычисления общей суммы для списка экземпляров 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.