Что такое 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