Почему println! молчит в тестах Rust? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: "Немой" 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 использует механизм перехвата файловых дескрипторов. Перед запуском каждого теста он:

Блоги

Рекомендаций по книгам