Тестирование в Rust: от юнит-тестов до property-based testing
Rust — один из немногих языков, где система типов и владения уже на этапе компиляции исключает целые классы ошибок (гонки данных, висячие указатели, двойное освобождение памяти). Однако логические ошибки, некорректная обработка краевых случаев и неверная бизнес-логика остаются. Для их выявления в экосистеме Rust сложился мощный набор инструментов тестирования — от встроенного тест-раннера до продвинутых фреймворков для генерации тестовых данных.
Встроенные средства: cargo test
Базовый инструмент тестирования встроен прямо в компилятор и менеджер пакетов. Для создания тестов достаточно добавить модуль с атрибутом #[cfg(test)] и пометить функции атрибутом #[test].
// src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn test_add_negative() {
// u64 не поддерживает отрицательные числа — это ошибка компиляции
// Но если бы функция принимала i64:
// assert_eq!(add(-2, 2), 0);
}
}
Запуск: cargo test. Флаг -- --nocapture выводит println! из тестов. Флаг -- --test-threads=1 отключает параллельный запуск.
Интеграционные тесты
Интеграционные тесты находятся в папке tests/ на уровне корня проекта. Каждый файл — отдельный крейт, который может тестировать публичный API библиотеки.
// tests/integration_test.rs
use my_library::add;
#[test]
fn test_integration() {
assert_eq!(add(10, 20), 30);
}
Для тестирования бинарных крейтов (с main.rs) можно использовать assert_cmd — он запускает бинарник как внешний процесс и проверяет stdout/stderr.
Property-based testing: proptest и quickcheck
Вместо ручного написания десятков тестовых случаев property-based testing генерирует тысячи случайных входных данных и проверяет инварианты. Два основных фреймворка:
- proptest — современный, гибкий, с поддержкой стратегий, фильтрации и сжатия примеров (shrink).
- quickcheck — более старый, вдохновлённый Haskell QuickCheck, проще, но менее мощный.
Пример с proptest:
use proptest::prelude::*;
fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
proptest! {
#[test]
fn test_reverse_twice(s in ".*") {
let reversed = reverse_string(&s);
let double_reversed = reverse_string(&reversed);
assert_eq!(s, double_reversed);
}
#[test]
fn test_reverse_length(s in ".*") {
let reversed = reverse_string(&s);
assert_eq!(s.len(), reversed.len());
}
}
Если тест падает, proptest находит минимальный контрпример (shrinking).
Тестирование асинхронного кода: tokio-test
Для асинхронных функций, работающих с tokio, обычные тесты не подходят — нужен асинхронный рантайм. tokio-test предоставляет макрос #[tokio::test].
use tokio::time::{sleep, Duration};
async fn delayed_add(a: u32, b: u32) -> u32 {
sleep(Duration::from_millis(10)).await;
a + b
}
#[tokio::test]
async fn test_delayed_add() {
let result = delayed_add(3, 5).await;
assert_eq!(result, 8);
}
Также можно использовать futures-test для работы с futures и async-std.
Моки и стабы: mockall и mockito
Для изоляции тестов от внешних зависимостей (БД, API, файловая система) используются моки.
- mockall — генерация моков на основе трейтов. Поддерживает ожидания (expectations) и проверку последовательности вызовов.
- mockito — HTTP-мок сервер для тестирования API-клиентов. Запускает локальный сервер на заданном порту.