Rust изначально проектировался с упором на надежность и безопасность, поэтому модульное тестирование стало неотъемлемой чистью экосистемы языка. Тесты интегрированы на уровне языка и тулчейна (cargo test), что делает их органичной частью процесса разработки.
История вопроса
В традиционных экосистемах (например C/C++, Python, Java) тестирование существовало стороной от самой программы. В Rust тесты — часть кода, они компилируются и проверяются как полноценный модуль. Такой синергии добиваются благодаря языковым конструкциям и особенностям компилятора.
Проблема
Без корректного тестирования невозможно гарантировать надежность ключевых функций. Для сложных и многомодульных проектов часто возникает вопрос: как удобно организовать тесты, не дать им зависеть от состояния других модулей и не усложнить структуру проекта?
Решение
В Rust тесты размещаются внутри самого исходного файла (с помощью #[cfg(test)]), либо в отдельной папке tests для интеграционного тестирования. К каждому модулю можно добавить приватные тесты, которые имеют доступ к приватному API.
Пример кода:
pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }
Ключевые особенности:
Для чего нужен use super::*; в модуле теста?
Чтобы тесты могли обращаться к функциям и структурам текущего модуля (в том числе к приватным), в тесте обычно используют use super::*;.
Может ли #[test] быть асинхронной?
В языке по умолчанию #[test] не поддерживает async, но с помощью внешних crate (например tokio или async-std) можно делать async #[test].
Пример кода:
#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }
Могут ли тесты менять глобальное состояние и как этого избежать?
Тесты в Rust по умолчанию исполняются параллельно, поэтому нельзя использовать разделяемое без синхронизации глобальное состояние (static mut), иначе возникнут гонки.
Все тесты используют одну глобальную переменную:
static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }
Плюсы:
Минусы:
Каждый тест изолирован, использует локальные переменные и mock-объекты.
Плюсы:
Минусы: