Тестирование программного кода — один из старейших и важнейших процессов в индустрии разработки. Однако во многих языках тестовые библиотеки поставляются отдельно, а интеграция тестов в основной код может быть непрозрачной или неудобной. В Rust с самых первых версий язык проектировался с поддержкой явного модульного тестирования «из коробки».
Во многих традиционных языках тесты располагаются отдельно или требуют настройки специфических фреймворков и сборщиков. Это усложняет совместную разработку, увеличивает риск рассинхронизации основной логики и тестов, а также усложняет проведение интеграционного и модульного тестирования.
Rust интегрирует систему тестирования прямо на уровне языка: модуль #[cfg(test)], директива #[test] и инструмент cargo test позволяют создавать, запускать и контролировать тесты в рамках исходного кода. Это обеспечивает тесную связь между кодом и проверяющими его тестами, гарантирует простоту запуска тестов и автоматизацию CI/CD процессов.
Пример кода:
// tests automatically compiled and run by `cargo test` #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(2 + 2, 4); } }
Ключевые особенности:
cargo test) запускает все обнаруженные тесты.#[cfg(test)] позволяет не включать тесты в релизную сборку.Можно ли использовать нестабильные или приватные функции в тестах, и как это сделать?
Да, можно. Внутри модуля тестов, объявленного как #[cfg(test)], тесты видят не только публичный, но и приватный API текущего модуля. Это означает, что вы можете тестировать детали реализации напрямую. Однако, из другого модуля — только через публичный интерфейс.
Пример:
fn private_internal(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn test_private() { assert_eq!(private_internal(41), 42); } }
Будут ли тесты с одинаковыми именами конфликтовать между разными модулями?
Нет, имена тестов видимы только внутри своего модуля. Тесты разных модулей могут называться одинаково, потому что компилятор различает их по полному пути (namespace).
Пример:
mod a { #[cfg(test)] mod tests { #[test] fn basic() {} } } mod b { #[cfg(test)] mod tests { #[test] fn basic() {} } }
Можно ли запускать отдельный тест или часть набора тестов?
Да, с помощью фильтрации имени теста: cargo test имя_теста. Это удобно для отладки больших наборов.
Пример:
cargo test only_this_test
Разработчик вынес тесты в отдельный проект без доступа к приватным функциям и с другой структурой каталогов. Из-за изменений в основной библиотеке тесты стали быстро отставать и терять актуальность.
Плюсы: Можно изолировать тестирование, избежать случайного попадания тестовых модулей в релизную сборку.
Минусы: Потеря синхронности, невозможность тестировать внутренние детали, высокая поддерживаемость и риск неактуальности тестов.
Тесты пишутся прямо в тех же модулях, что и основная логика, малые атомарные тест-кейсы тестируют приватный и публичный API. Новичок быстро вникает в работу модуля через тесты.
Плюсы: Максимальная поддержка синхронности, прозрачность, ускоренное локальное тестирование, пригодность для тест-дривен разработки.
Минусы: Увеличение числа строк кода в исходных файлах, потенциальный рост сложности при большом количестве тестов.