Как правильно определять зависимости только для тестов в Rust/Cargo?
Правильное разделение зависимостей в Cargo
В Rust есть встроенная поддержка тестовых зависимостей через раздел [dev-dependencies] в Cargo.toml. Это стандартный механизм, который полностью решает вашу проблему. Давайте разберёмся, как его правильно использовать.
1. Базовая настройка Cargo.toml
Поместите крейты, нужные только для тестов, в секцию [dev-dependencies]:
[package]
name = "my_project"
version = "0.1.0"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
reqwest = "0.11"
[dev-dependencies]
rand = "0.8"
tempfile = "3.3"
mockall = "0.11"
Теперь rand, tempfile и mockall будут загружаться и компилироваться только при сборке тестов (команда cargo test). В production-сборке (cargo build --release) эти крейты полностью исключаются.
2. Импорт в тестовом коде
В тестовых модулях используйте стандартные use-директивы. Cargo автоматически разрешает импорт из [dev-dependencies] внутри тестовых контекстов:
// src/lib.rs
pub fn process_data(input: &str) -> String {
format!("Processed: {}", input)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng; // Работает! Крейт доступен только в тестах.
use tempfile::TempDir;
#[test]
fn test_random_processing() {
let mut rng = rand::thread_rng();
let random_num: u32 = rng.gen_range(1..100);
let result = process_data(&random_num.to_string());
assert!(result.contains("Processed"));
}
#[test]
fn test_with_temp_file() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(&file_path, b"test data").unwrap();
assert!(file_path.exists());
}
}
3. Интеграционные тесты
Для интеграционных тестов (в папке tests/) импорт работает так же — Cargo видит [dev-dependencies] во всех тестовых контекстах:
// tests/integration_test.rs
use my_project::process_data;
use rand::Rng; // Тоже работает!
#[test]
fn integration_test_with_rand() {
let mut rng = rand::thread_rng();
let value = rng.gen::<f64>();
let result = process_data(&value.to_string());
assert!(!result.is_empty());
}
4. Условная компиляция с #[cfg(test)]
Если нужно использовать тестовую зависимость в production-коде (например, для моков), оберните импорт в #[cfg(test)]. Но это редко требуется — обычно тестовые крейты используются только в тестовых модулях:
// src/lib.rs
#[cfg(test)]
use mockall::automock; // Доступно только при тестировании
#[cfg_attr(test, automock)]
pub trait DataProvider {
fn get_data(&self) -> String;
}
pub fn use_provider(provider: &dyn DataProvider) -> String {
provider.get_data()
}
5. Проверка и отладка
Чтобы убедиться, что зависимости действительно исключены из production-сборки, выполните:
cargo build --release --verbose
Вы не увидите в логах компиляции rand или других dev-крейтов. Для тестовой сборки:
cargo test --verbose
Здесь dev-зависимости будут загружены и скомпилированы.
Важные моменты
- Не используйте
[dependencies]для тестовых крейтов — это увеличит размер финального бинарника. - Не путайте с
[build-dependencies]— это для build-скриптов, не для тестов. - Версионирование: указывайте конкретные версии в
[dev-dependencies], как и в обычных. - Feature-флаги: если крейт нужен и в production, и в тестах, но с разными фичами, используйте
[dependencies]с базовыми фичами, а в[dev-dependencies]— с дополнительными.
Этот подход полностью решает вашу проблему: тестовые зависимости не влияют на production-сборку, а код остаётся чистым и понятным.
<<Альтернативный подход: условная компиляция с feature-флагами
Если вам нужно более гибкое управление зависимостями (например, включать тестовые крейты в определённых сценариях), используйте feature-флаги Cargo. Это полезно, когда один и тот же крейт нужен и в тестах, и в production, но с разной конфигурацией.
Пример настройки
[package]
name = "my_project"
version = "0.1.0"
[features]
testing = ["rand", "tempfile"] # Включаем крейты для тестового режима
[dependencies]
rand = { version = "0.8", optional = true }
tempfile = { version = "3.3", optional = true }
serde = "1.0"
В коде используйте условную компиляцию:
// src/lib.rs
#[cfg(feature = "testing")]
use rand::Rng;
pub fn generate_random() -> u32 {
#[cfg(feature = "testing")]
{
let mut rng = rand::thread_rng();
rng.gen()
}
#[cfg(not(feature = "testing"))]
{
42 // Значение по умолчанию для production
}
}
Запускайте тесты с включённым feature-флагом:
cargo test --features testing
Плюсы: полный контроль над тем, когда и какие зависимости подключаются. Минусы: больше кода и сложность конфигурации. Для большинства случаев стандартный [dev-dependencies] — более простое и идиоматичное решение.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги