Property-based тестирование в Java: ломаем код быстрее, чем Junit

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Property-based тестирование в Java: когда один тест заменяет тысячу

Вы когда-нибудь писали юнит-тест, который проходил зелёным, а через неделю в проде падал с ошибкой на «каком-то странном значении»? Знакомо. Обычное (example-based) тестирование — это как проверять, что чайник закипает, только при 100°C. А что, если давление другое? А если высота над уровнем моря? Property-based тестирование в Java решает именно эту проблему: вместо того чтобы вручную перебирать примеры, вы описываете свойства, которым должен удовлетворять ваш код, а фреймворк генерирует сотни случайных входных данных и проверяет, всё ли в порядке.

Этот подход особенно полезен для Java разработчиков, которые пишут сложные алгоритмы, работают с коллекциями, парсерами или математическими функциями. В этой статье мы разберём, как внедрить property-based testing в ваш проект с помощью библиотеки jqwik, и покажем реальные примеры кода.

Что такое property-based тестирование и чем оно отличается от обычного?

В классическом подходе вы пишете:

@Test
void testAddition() {
    assertEquals(4, Calculator.add(2, 2));
    assertEquals(0, Calculator.add(-1, 1));
}

Всё хорошо, пока кто-то не передаст Integer.MAX_VALUE. Или null. Или очень большое отрицательное число. Property-based тестирование переворачивает логику: вы говорите не «ожидаю такой-то результат», а «для любых корректных входных данных результат удовлетворяет свойству X».

Например, свойство для сложения: add(a, b) == add(b, a) (коммутативность). Или add(a, 0) == a (нейтральный элемент). Фреймворк генерирует тысячи случайных пар a и b и проверяет, не нарушается ли свойство.

Основные преимущества property-based testing:

  • Автоматическая генерация тестовых данных — вы находите баги, о которых даже не думали.
  • Сокращение количества тестов — один property-тест заменяет десятки example-based.
  • Проверка граничных случаев — генераторы умеют создавать «опасные» значения (0, null, пустые строки, большие числа).
  • Shrinking — если тест упал, фреймворк минимизирует входные данные до самого простого примера, на котором воспроизводится ошибка.

Библиотеки для property-based тестирования в Java

На рынке есть несколько зрелых решений:

  • jqwik — современная, легковесная, отлично интегрируется с JUnit 5. Рекомендую для старта.
  • QuickTheories — быстрая, но менее популярная.
  • JUnit QuickCheck — форк старого Scala-инструмента, но поддержка хромает.

Мы будем использовать jqwik, так как у него самый удобный API и отличная документация. Добавьте зависимость в pom.xml:

<dependency>
    <groupId>net.jqwik</groupId>
    <artifactId>jqwik</artifactId>
    <version>1.8.4</version>
    <scope>test</scope>
</dependency>

Или для Gradle:

testImplementation 'net.jqwik:jqwik:1.8.4'

Пишем первый property-based тест на Java

Допустим, у нас есть метод, который разворачивает строку:

public static String reverse(String s) {
    if (s == null) return null;
    return new StringBuilder(s).reverse().toString();
}

Какое свойство можно проверить? Если развернуть строку дважды, мы получим исходную строку. Это идеальное свойство для property-based testing.

import net.jqwik.api.*;
import static org.assertj.core.api.Assertions.assertThat;

class StringProperties {

    @Property
    void reverseTwiceReturnsOriginal(@ForAll String anyString) {
        String reversed = StringUtils.reverse(anyString);
        String doubleReversed = StringUtils.reverse(reversed);
        assertThat(doubleReversed).isEqualTo(anyString);
    }
}

Что здесь происходит? @ForAll String anyString — jqwik автоматически генерирует случайные строки: пустые, с пробелами, с Unicode-символами, null (если разрешено). Тест прогоняется 1000 раз (по умолчанию) с разными значениями. Если хотя бы один раз свойство нарушится — тест упадёт, и jqwik покажет минимальный пример, на котором ошибка воспроизводится.

Shrinking в действии

Представьте, что наша реализация reverse содержит баг: если строка содержит символ 'я', метод падает с исключением. jqwik найдёт это случайно, а затем начнёт «сжимать» входные данные, пока не получит самую короткую строку с этим символом — например, "я". Вы получите отчёт:

Sample: "я"
Shrunk to: "я" (1 steps)

Это невероятно экономит время при отладке.

Сложные свойства: работа с коллекциями

Допустим, мы пишем метод сортировки:

public static List<Integer> sort(List<Integer> list) {
    List<Integer> sorted = new ArrayList<>(list);
    Collections.sort(sorted);
    return sorted;
}

Какие свойства можно проверить?

  • Результат не длиннее исходного списка.
  • Результат отсортирован (каждый следующий элемент >= предыдущего).
  • Все элементы исходного списка присутствуют в отсортированном.

Реализуем:

@Property
void sortPreservesElements(@ForAll List<@IntRange(min = -1000, max = 1000) Integer> numbers) {
    List<Integer> sorted = ListUtils.sort(numbers);
    assertThat(sorted).containsExactlyInAnyOrderElementsOf(numbers);
}

@Property
void sortReturnsSortedList(@ForAll List<Integer> numbers) {
    List<Integer> sorted = ListUtils.sort(numbers);
    for (int i = 0; i < sorted.size() - 1; i++) {
        assertThat(sorted.get(i)).isLessThanOrEqualTo(sorted.get(i + 1));
    }
}

Обратите внимание на @IntRange — это ограничивает генерацию целых чисел заданным диапазоном. Вы можете тонко настраивать генераторы: @StringLength, @Size, @UniqueElements и т.д.

Property-based тестирование для алгоритмов и математики

Ещё одна классическая задача — тестирование математических функций. Например, функция sqrt:

public static double                

Блоги

Рекомендаций по книгам