Вступление: "Немой" println! — знакомая боль каждого новичка в Rust
Вы пишете свой первый unit тест на Rust. Всё логично: создаёте структуру, реализуете метод, а затем — торжественный момент проверки. Вставляете println! в тест, чтобы убедиться, что данные верны. Запускаете cargo test... И видите только сухую строчку: "test result: ok. 1 passed; 0 failed". Никаких "Hello, world!", никаких промежуточных значений. Ваш println! просто исчез.
Знакомо? Добро пожаловать в клуб. Этот вопрос на Stack Overflow собрал тысячи просмотров, и он — настоящий камень преткновения для всех, кто изучает тестирование в Rust. В этой статье мы не просто скажем "используй --nocapture". Мы разберём, почему так происходит, как работает захват вывода (capture) в тестовом раннере, и как правильно отлаживать код, не нарушая принципов модульного тестирования.
Почему Rust "затыкает" println! в тестах?
Дело не в баге и не в злом умысле создателей языка. Это осознанное архитектурное решение. Когда вы запускаете cargo test, Rust делает тесты чистыми и предсказуемыми. Представьте, что у вас 500 тестов, и каждый из них выводит по 10 строк отладки в stdout. Ваш терминал превратится в кашу, а главное — вы не сможете отличить реальные ошибки от отладочного мусора.
По умолчанию тестовый раннер (библиотека test) перехватывает весь стандартный вывод (std::io::stdout()) и стандартный вывод ошибок (std::io::stderr()). Он делает это на уровне системных дескрипторов. println! пишет именно в stdout, поэтому его вывод попадает в "чёрный ящик" раннера. Вывод показывается только если тест провалился — чтобы вы могли увидеть, что пошло не так.
Как это выглядит в коде?
Давайте посмотрим на простой пример, который воспроизводит проблему:
// src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_two() {
println!("Я выполняюсь, но меня не видно!");
let result = add_two(2);
println!("Результат: {}", result);
assert_eq!(result, 4);
}
}
Запускаем:
$ cargo test
Compiling my_project v0.1.0
Finished test [unoptimized + debuginfo] target(s) in 0.5s
Running unittests src/lib.rs (target/debug/deps/my_project-abc123)
running 1 test
test tests::test_add_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Как видите, ни одно из сообщений println! не появилось. Тест прошёл успешно, и раннер решил, что показывать вам нечего.
Как "разбудить" println!? Способы и подводные камни
Существует три основных подхода, и каждый из них хорош для своей ситуации.
Способ 1: Флаг --nocapture (самый простой и популярный)
Это то, что вы найдёте в первом ответе на Stack Overflow. Флаг --nocapture отключает перехват вывода для всех тестов в текущем запуске.
$ cargo test -- --nocapture
Обратите внимание на двойное тире --. Оно разделяет аргументы для cargo и для самого тестового бинарника. Всё, что после --, передаётся раннеру тестов.
Теперь вывод будет таким:
$ cargo test -- --nocapture
Compiling my_project v0.1.0
Finished test [unoptimized + debuginfo] target(s) in 0.5s
Running unittests src/lib.rs (target/debug/deps/my_project-abc123)
running 1 test
Я выполняюсь, но меня не видно!
Результат: 4
test tests::test_add_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Плюсы: Мгновенно, не требует изменения кода. Минусы: Вывод всех тестов смешивается. Если у вас сотня тестов, найти нужный — та ещё задача.
Способ 2: Использование stderr (для постоянного логирования)
Если вы хотите, чтобы отладочные сообщения всегда были видны, даже без --nocapture, пишите не в stdout, а в stderr. eprintln! по умолчанию не захватывается тестовым раннером (хотя это поведение может меняться в зависимости от версии и конфигурации).
#[test]
fn test_with_eprintln() {
eprintln!("Это сообщение в stderr! Его будет видно всегда.");
assert_eq!(2 + 2, 4);
}
Важно: В некоторых конфигурациях CI или при запуске с определёнными флагами stderr тоже может быть захвачен. Но для локальной разработки это рабочий вариант.
Способ 3: Условная компиляция (для профи)
Самый правильный подход — не полагаться на println! в тестах вообще. Используйте assert! и assert_eq! для проверок. Если нужно вывести сложную структуру для отладки — используйте макрос dbg!.
#[test]
fn test_with_dbg() {
let data = vec![1, 2, 3, 4];
let processed: Vec<i32> = data.iter().map(|x| x * 2).collect();
// dbg! выводит и значение, и файл, и строку, но только если тест провалится
// или при --nocapture
dbg!(&processed);
assert_eq!(processed, vec![2, 4, 6, 8]);
}
dbg! — это "умный" println!. Он выводит в stderr, возвращает значение обратно (что удобно для встраивания в выражения) и показывает имя переменной. Но он всё ещё подчиняется правилам захвата вывода.
Почему это важно для обучения? Глубокое погружение в механику
Понимание того, почему println! не работает в Rust unit тестах, — это не просто решение проблемы "как увидеть вывод". Это ключ к пониманию философии тестирования в Rust.
Как работает захват (capture) под капотом?
Тестовый раннер Rust использует механизм перехвата файловых дескрипторов. Перед запуском каждого теста он: