Proptest для Rust: полное руководство с примерами

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

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

Начать курс

Что такое Proptest и зачем он нужен?

Proptest — это мощная библиотека для property-based тестирования (тестирования на основе свойств) на языке Rust. В отличие от традиционного unit-тестирования, где вы вручную задаёте конкретные входные данные и ожидаемые результаты, property-based тестирование позволяет вам описывать свойства, которым должна удовлетворять ваша программа для любых корректных входных данных. Библиотека автоматически генерирует множество случайных тестовых случаев и проверяет, выполняются ли заданные свойства.

Зачем это нужно?

  • Обнаружение скрытых багов: Случайные данные часто находят краевые случаи (edge cases), о которых разработчик даже не подумал.
  • Сокращение объёма тестового кода: Вместо написания десятков примеров, вы пишете одно свойство.
  • Автоматическая минимизация (shrinking): Если тест падает, Proptest автоматически пытается найти минимальный набор входных данных, который воспроизводит ошибку.
  • Документирование спецификации: Свойства — это формальные, проверяемые утверждения о поведении кода.

Proptest вдохновлён библиотекой QuickCheck из Haskell, но адаптирован под особенности Rust, включая работу с владением, заимствованием и типажами.

Установка

Proptest — это библиотека для Rust, поэтому она устанавливается через менеджер пакетов Cargo. Добавьте её в раздел [dev-dependencies] вашего файла Cargo.toml:

[dev-dependencies]
proptest = "1.4"

После этого выполните команду cargo build или cargo test, чтобы загрузить и скомпилировать библиотеку. Proptest используется только во время тестирования, поэтому не увеличивает размер вашего финального бинарника.

Для работы с асинхронным кодом (tokio, async-std) может потребоваться дополнительная настройка, но в большинстве случаев стандартной установки достаточно.

Быстрый старт с примером кода

Давайте напишем простой тест, который проверяет, что функция reverse (разворачивание строки) является инволюцией: двойное применение возвращает исходную строку.

// src/lib.rs
pub fn reverse(s: &str) -> String {
    s.chars().rev().collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn test_reverse_involution(s in ".*") { // ".*" — любая строка
            let reversed = reverse(&s);
            let double_reversed = reverse(&reversed);
            prop_assert_eq!(s, double_reversed);
        }
    }
}

Запустите тесты: cargo test. Proptest сгенерирует 256 случайных строк (по умолчанию) и проверит, что свойство выполняется. Если тест упадёт, вы увидите сообщение с минимальным воспроизводящим примером.

Основные методы и функции

Ниже приведены ключевые компоненты Proptest. Для каждого дано описание, параметры и пример.

1. proptest! — макрос для определения тестов

Описание: Главный макрос, который запускает property-based тесты. Он принимает блок с тестовыми функциями и автоматически генерирует входные данные.

Параметры: Внутри макроса вы определяете тесты с атрибутом #[test]. Входные данные задаются в виде param in strategy.

Пример:

proptest! {
    #[test]
    fn test_addition_commutative(a in 0..100i32, b in 0..100i32) {
        prop_assert_eq!(a + b, b + a);
    }
}

2. prop_assert! и prop_assert_eq! — утверждения

Описание: Аналоги стандартных assert! и assert_eq!, но адаптированные для Proptest. Они не паникуют, а возвращают ошибку, которую Proptest может обработать для минимизации.

Параметры: prop_assert!(condition) — проверяет истинность условия. prop_assert_eq!(left, right) — проверяет равенство.

Пример:

proptest! {
    #[test]
    fn test_vec_len(v in prop::collection::vec(1..10i32, 0..100)) {
        let len = v.len();
        prop_assert!(len <= 100, "Length {} exceeds 100", len);
        prop_assert_eq!(v.len(), v.iter().count());
    }
}

3. any::<T>() — стратегия для произвольного значения

Описание: Генерирует случайное значение любого типа, реализующего типаж Arbitrary. Большинство стандартных типов (числа, строки, векторы) уже реализуют его.

Параметры: Тип T.

Пример:

proptest! {
    #[test]
    fn test_any_int(x in any::<i32>()) {
        prop_assert!(x >= i32::MIN && x <= i32::MAX);
    }
}

4. prop::collection::vec — стратегия для векторов

Описание: Генерирует вектор, элементы которого создаются по заданной стратегии, а длина варьируется в заданном диапазоне.

Параметры: vec(strategy, size_range). strategy — стратегия для элементов, size_range — диапазон длин (например, 0..100).

Пример:

proptest! {
    #[test]
    fn test_vec_sort(v in prop::collection::vec(0..1000i32, 0..50)) {
        let mut sorted = v.clone();
        sorted.sort();
        for pair in sorted.windows(2) {
            prop_assert!(pair[0] <= pair[1]);
        }
    }
}

5. prop::string::string_regex — строки по регулярному выражению

Описание: Генерирует строки, соответствующие заданному регулярному выражению. Очень полезно для тестирования парсеров или валидаторов.

Параметры: Строковый срез с регулярным выражением.

Пример:

proptest! {
    #[test]
    fn test_email_regex(email in prop::string::string_regex("[a-z]+@[a-z]+\\.[a-z]{2,4}")) {
        prop_assert!(email.contains('@'));
    }
}

6. prop::option::of — стратегия для Option

Описание: Генерирует Option<T>, где None появляется с заданной вероятностью (по умолчанию 50%).

Параметры: prop::option::of(strategy).

Пример:

proptest! {
    #[test]
    fn test_option_map(opt in prop::option::of(1..10i32)) {
        let result = opt.map(|x| x * 2);
        if let Some(val) = opt {
            prop_assert_eq!(result, Some(val * 2));
        } else {
            prop_assert_eq!(result, None);
        }
    }
}

7. prop::result::maybe_ok — стратегия для Result

Описание: Генерирует Result<T, E>, где успешный результат появляется с заданной вероятностью.

Параметры: prop::result::maybe_ok(ok_strategy, err_strategy, probability).

Пример:

proptest! {
    #[test]
    fn test_result_unwrap(res in prop::result::maybe_ok("success", "error", 0.8)) {
        if res.is_ok() {
            prop_assert_eq!(res.unwrap(), "success");
        }
    }
}

8. prop::sample::select — выбор из списка

Описание: Случайным образом выбирает одно значение из заданного набора.

Параметры: prop::sample::select(&[values]).

Пример:

proptest! {
    #[test]
    fn test_operation(op in prop::sample::select(&["add", "sub", "mul"])) {
        let a = 10i32;
        let b = 5i32;
        match op {
            "add" => prop_assert_eq!(a + b, 15),
            "sub" => prop_assert_eq!(a - b, 5),
            "mul" => prop_assert_eq!(a * b, 50),
            _ => unreachable!(),
        }
    }
}

9. prop::num::f64::ANY — стратегия для чисел с плавающей точкой

Описание: Генерирует любое значение f64, включая NaN, бесконечность и отрицательный ноль. Есть также POSITIVE, NEGATIVE, NORMAL и другие.

Параметры: Константа.

Пример:

proptest! {
    #[test]
    fn test_f64_is_finite(x in proptest::num::f64::POSITIVE) {
        prop_assert!(x.is_finite());
    }
}

10. .prop_map — трансформация стратегий

Описание: Позволяет преобразовать значения, сгенерированные одной стратегией, в другие значения.

Параметры: Замыкание, которое принимает исходное значение и возвращает новое.

Пример:

proptest! {
    #[test]
    fn test_doubled(x in (0..100i32).prop_map(|v| v * 2)) {
        prop_assert!(x % 2 == 0);
    }
}

Реальный практический пример использования

Предположим, вы пишете библиотеку для работы с датами и временем. У вас есть функция add_days, которая прибавляет к дате заданное количество дней. Вы хотите проверить, что:

  • Результат всегда является корректной датой.
  • Прибавление 0 дней не меняет дату.
  • Прибавление N дней, а затем вычитание N дней возвращает исходную дату.
use chrono::NaiveDate;

pub fn add_days(date: NaiveDate, days: i64) -> NaiveDate {
    date + chrono::Duration::days(days)
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    // Стратегия для генерации случайных дат
    fn date_strategy() -> impl Strategy<Value = NaiveDate> {
        (1970i32..2100, 1u32..13, 1u32..29)
            .prop_map(|(y, m, d)| NaiveDate::from_ymd_opt(y, m, d).unwrap())
    }

    proptest! {
        #[test]
        fn test_add_days_identity(date in date_strategy()) {
            let result = add_days(date, 0);
            prop_assert_eq!(date, result);
        }

        #[test]
        fn test_add_days_roundtrip(date in date_strategy(), days in -365..365i64) {
            let added = add_days(date, days);
            let back = add_days(added, -days);
            prop_assert_eq                    

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