AssertJ для Java: Полное руководство по Fluent Assertions
1. Что это и зачем нужна
AssertJ — это мощная библиотека fluent-assertions (текучих утверждений) для языка Java, предназначенная для написания читаемых, выразительных и самодокументируемых тестов. В отличие от стандартного JUnit или TestNG, которые предоставляют лишь базовые методы вроде assertEquals(a, b), AssertJ предлагает богатый API с цепочками вызовов, что делает код тестов похожим на естественный язык. Например, вместо assertTrue(list.contains("foo")) вы пишете assertThat(list).contains("foo").
Зачем она нужна? Во-первых, AssertJ значительно улучшает читаемость тестов. Когда разработчик видит assertThat(result).isNotNull().hasSize(5).contains("expected"), он сразу понимает, что проверяется: результат не null, размер равен 5, и он содержит строку "expected". Во-вторых, AssertJ предоставляет детальные сообщения об ошибках. Вместо скучного "expected true but was false" вы получите "Expected list to contain 'foo' but could not find it in [bar, baz]". В-третьих, библиотека поддерживает множество типов данных: коллекции, строки, числа, даты, файлы, исключения и даже пользовательские объекты.
AssertJ особенно полезна в проектах, где важна поддержка тестовой базы. Благодаря fluent API, тесты становятся менее хрупкими при рефакторинге, так как цепочки методов легко модифицировать. Библиотека активно развивается, имеет отличную документацию и широко используется в индустрии (Spring, Hibernate, Netflix и др.).
2. Установка
AssertJ распространяется через Maven Central. Для использования в проекте добавьте зависимость в ваш pom.xml (Maven) или build.gradle (Gradle).
Maven
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version> <!-- Проверьте актуальную версию на Maven Central -->
<scope>test</scope>
</dependency>
Gradle
testImplementation 'org.assertj:assertj-core:3.25.3'
Если вы используете JUnit 5, дополнительных зависимостей не требуется. Для JUnit 4 AssertJ также работает без проблем. Убедитесь, что версия AssertJ совместима с вашей версией Java (3.x требует Java 8 или выше).
3. Быстрый старт — минимальный рабочий пример
После добавления зависимости вы можете сразу начать использовать AssertJ. Основной метод — assertThat(), который принимает объект и возвращает соответствующий assertion-объект.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
public class QuickStartTest {
@Test
void testSimpleAssertion() {
String name = "AssertJ";
// Базовое утверждение
assertThat(name)
.isNotNull()
.isNotEmpty()
.startsWith("Ass")
.endsWith("J")
.hasSize(7);
// Проверка чисел
int number = 42;
assertThat(number)
.isPositive()
.isGreaterThan(40)
.isLessThan(50);
// Проверка коллекций
var list = java.util.List.of("a", "b", "c");
assertThat(list)
.hasSize(3)
.contains("a", "b")
.doesNotContain("z");
}
}
Этот пример демонстрирует ключевую идею: цепочка методов, каждый из которых возвращает тот же assertion-объект, позволяя писать последовательные проверки. Если любое утверждение не выполняется, тест падает с понятным сообщением.
4. Основные методы/функции/классы
AssertJ предоставляет сотни методов. Ниже приведены 8 наиболее часто используемых с сигнатурами, описанием и примерами.
4.1. isEqualTo
Сигнатура: AbstractAssert isEqualTo(Object expected)
Что делает: Проверяет, что фактическое значение равно ожидаемому (через equals()). Для массивов и коллекций сравнивает содержимое.
@Test
void testIsEqualTo() {
String actual = "Hello";
assertThat(actual).isEqualTo("Hello");
int[] numbers = {1, 2, 3};
assertThat(numbers).isEqualTo(new int[]{1, 2, 3}); // Работает для массивов
}
4.2. containsExactly
Сигнатура: AbstractIterableAssert containsExactly(E... values)
Что делает: Проверяет, что коллекция содержит ровно указанные элементы в том же порядке. Идеально для проверки порядка элементов.
@Test
void testContainsExactly() {
var list = java.util.List.of("apple", "banana", "cherry");
assertThat(list).containsExactly("apple", "banana", "cherry");
// Если порядок не важен, используйте containsExactlyInAnyOrder
assertThat(list).containsExactlyInAnyOrder("cherry", "apple", "banana");
}
4.3. hasSize
Сигнатура: AbstractIterableAssert hasSize(int expected)
Что делает: Проверяет размер коллекции, строки, массива или Map.
@Test
void testHasSize() {
var list = java.util.List.of(1, 2, 3);
assertThat(list).hasSize(3);
String text = "AssertJ";
assertThat(text).hasSize(7);
var map = java.util.Map.of("key1", "value1", "key2", "value2");
assertThat(map).hasSize(2);
}
4.4. extracting
Сигнатура: AbstractListAssert extracting(Function<T, Object> extractor)
Что делает: Извлекает поле или результат метода из каждого элемента коллекции и позволяет проверять извлеченные значения. Мощный инструмент для работы с POJO.
@Test
void testExtracting() {
record Person(String name, int age) {}
var people = java.util.List.of(
new Person("Alice", 30),
new Person("Bob", 25)
);
assertThat(people)
.extracting(Person::name)
.contains("Alice", "Bob");
// Можно извлекать несколько полей
assertThat(people)
.extracting(Person::name, Person::age)
.contains(
tuple("Alice", 30),
tuple("Bob", 25)
);
}
4.5. filteredOn
Сигнатура: AbstractListAssert filteredOn(Predicate<T> predicate)
Что делает: Фильтрует коллекцию по условию и возвращает новый assertion для отфильтрованных элементов. Позволяет проверять подмножества.
@Test
void testFilteredOn() {
record Product(String name, double price) {}
var products = java.util.List.of(
new Product("Laptop", 1200.0),
new Product("Mouse", 25.0),
new Product("Keyboard", 80.0)
);
assertThat(products)
.filteredOn(p -> p.price() > 100)
.extracting(Product::name)
.containsExactly("Laptop");
}
4.6. isInstanceOf
Сигнатура: AbstractAssert isInstanceOf(Class<?> type)
Что делает: Проверяет, что объект является экземпляром указанного класса (или его подкласса). Полезно при работе с полиморфизмом.
@Test
void testIsInstanceOf() {
Object value = "Hello";
assertThat(value).isInstanceOf(String.class);
// Для точного совпадения класса используйте isExactlyInstanceOf
assertThat(value).isExactlyInstanceOf(String.class);
// Проверка интерфейсов
assertThat(value).isInstanceOf(CharSequence.class);
}
4.7. matches
Сигнатура: AbstractStringAssert matches(Pattern pattern) или matches(String regex)
Что делает: Проверяет, что строка соответствует регулярному выражению. Также есть doesNotMatch для обратной проверки.
@Test
void testMatches() {
String email = "user@example.com";
assertThat(email).matches("^[A-Za-z0-9+_.-]+@(.+)$");
// С использованием Pattern
var pattern = java.util.regex.Pattern.compile("\\d{3}-\\d{2}-\\d{4}");
assertThat("123-45-6789").matches(pattern);
}
4.8. isCloseTo (для чисел с плавающей точкой)
Сигнатура: AbstractDoubleAssert isCloseTo(double expected, Offset offset) или isCloseTo(double expected, Percentage percentage)
Что делает: Проверяет, что число с плавающей точкой находится в пределах заданного смещения или процента от ожидаемого значения. Решает проблему сравнения double/float.
@Test
void testIsCloseTo() {
double actual = 3.14159;
assertThat(actual).isCloseTo(3.14, within(0.01)); // Offset.of(0.01)
assertThat(actual).isCloseTo(3.14, withinPercentage(1.0)); // 1% tolerance
// Для float
float f = 0.1f + 0.2f;
assertThat(f).isCloseTo(0.3f, within(0.0001f));
}
Примечание: within и withinPercentage импортируются из org.assertj.core.api.Assertions.within и withinPercentage.
5. Реальный практический пример (100-200 строк кода)
Рассмотрим тестирование сервиса заказов интернет-магазина. У нас есть класс OrderService, который обрабатывает заказы, и мы хотим написать комплексные тесты с использованием AssertJ.
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.tuple;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
// Модельные классы
record OrderItem(String productId, String name, int quantity, BigDecimal price) {}
record Order(String id, String customerEmail, List<OrderItem> items,
LocalDateTime createdAt, OrderStatus status) {}
enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
// Сервис, который мы тестируем
class OrderService {
private final Map<String, Order> orders = new HashMap<>();
public Order createOrder(String customerEmail, List<OrderItem> items) {
String id = UUID.randomUUID().toString();
Order order = new Order(id, customerEmail, items, LocalDateTime.now(), OrderStatus.PENDING);
orders.put(id, order);
return order;
}
public Order confirmOrder(String orderId) {
Order order = orders.get(orderId);
if (order == null) throw new IllegalArgumentException("Order not found");
Order confirmed = new Order(order.id(), order.customerEmail(), order.items(),
order.createdAt(), OrderStatus.CONFIRMED);
orders.put(orderId, confirmed);
return confirmed;
}
public List<Order> findOrdersByCustomer(String email) {
return orders.values().stream()
.filter(o -> o.customerEmail().equals(email))
.toList();
}
public BigDecimal calculateTotal(String orderId) {
Order order = orders.get(orderId);
if (order == null) throw new IllegalArgumentException("Order not found");
return order.items().stream()
.map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// Тесты
class OrderServiceTest {
private OrderService orderService;
private List<OrderItem> sampleItems;
@BeforeEach
void setUp() {
orderService = new OrderService();
sampleItems = List.of(
new OrderItem("P001", "Laptop", 1, new BigDecimal("1200.00")),
new OrderItem("P002", "Mouse", 2, new BigDecimal("25.00")),
new OrderItem("P003", "Keyboard", 1, new BigDecimal("80.00"))
);
}
@Test
void testCreateOrder() {
Order order = orderService.createOrder("customer@test.com", sampleItems);
// Проверяем, что заказ создан корректно
assertThat(order)
.isNotNull()
.satisfies(o -> {
assertThat(o.id()).isNotEmpty();
assertThat(o.customerEmail()).isEqualTo("customer@test.com");
assertThat(o.status()).isEqualTo(OrderStatus.PENDING);
assertThat(o.createdAt()).isBeforeOrEqualTo(LocalDateTime.now());
});
// Проверяем элементы заказа
assertThat(order.items())
.hasSize(3)
.extracting(OrderItem::productId, OrderItem::name, OrderItem::quantity)
.containsExactly(
tuple("P001", "Laptop", 1),
tuple("P002", "Mouse", 2),
tuple("P003", "Keyboard", 1)
);
}
@Test
void testConfirmOrder() {
Order created = orderService.createOrder("customer@test.com", sampleItems);
Order confirmed = orderService.confirmOrder(created.id());
// Проверяем, что статус изменился
assertThat(confirmed)
.isNotNull()
.hasFieldOrPropertyWithValue("status", OrderStatus.CONFIRMED)
.hasFieldOrPropertyWithValue("customerEmail", "customer@test.com");
// Проверяем, что оригинальный заказ не изменился (иммутабельность)
assertThat(created.status()).isEqualTo(OrderStatus.PENDING);
}
@Test
void testFindOrdersByCustomer() {
orderService.createOrder("alice@test.com", sampleItems);
orderService.createOrder("bob@test.com", List.of(
new OrderItem("P Также в библиотеке
PDF генерация в JavaScript: библиотека pdf-lib
HTTP-клиент для TypeScript: топ библиотек для современных веб-приложений
Java для геолокации и карт: библиотеки, API и примеры
Очереди задач в Kotlin: Coroutines, KJob и продвинутые паттерны