Avatar ·

Как убрать предупреждение "function is never used" для функции, используемой только в тестах?

Привет, коллеги! Я только начал изучать Rust и столкнулся с раздражающей проблемой. У меня есть небольшая утилита, и я написал для неё вспомогательную функцию, которая нужна только в тестах. Например, чтобы подготовить тестовые данные. Вот упрощённый код: ```rust fn prepare_test_data() -> Vec { vec![1, 2, 3, 4, 5] } #[cfg(test)] mod tests { use super::*; #[test] fn test_with_data() { let data = prepare_test_data(); assert_eq!(data.len(), 5); } } ``` Когда я запускаю `cargo build` (не тесты, а обычную сборку), компилятор выдаёт предупреждение: ``` warning: function `prepare_test_data` is never used --> src/lib.rs:1:1 | 1 | fn prepare_test_data() -> Vec { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default ``` Я понимаю, что функция действительно не используется в основном коде, но она же нужна для тестов! Как правильно подавить это предупреждение? Я пробовал ставить `#[allow(dead_code)]` над функцией, но это кажется костылём. Есть ли более идиоматичный способ в Rust? Может быть, я неправильно организую код? Заранее спасибо за помощь! <<>>

Проблема и её корень

Вы столкнулись с классической ситуацией в Rust. Компилятор видит, что функция prepare_test_data определена в основном модуле, но не вызывается из основного кода. Модуль тестов #[cfg(test)] компилируется только при запуске cargo test. При обычной сборке (cargo build) этот модуль полностью игнорируется, поэтому компилятор считает функцию мёртвым кодом.

Есть три правильных способа решить эту проблему, каждый из которых подходит для разных ситуаций.

Способ 1: Перенести функцию в модуль тестов (рекомендуемый)

Самый идиоматичный подход — поместить вспомогательную функцию внутрь модуля tests или в отдельный вложенный модуль, который тоже аннотирован #[cfg(test)]. Тогда компилятор будет видеть её только во время тестирования.

// main.rs или lib.rs

#[cfg(test)]
mod tests {
    // Функция теперь живёт только в тестовом контексте
    fn prepare_test_data() -> Vec<i32> {
        vec![1, 2, 3, 4, 5]
    }

    #[test]
    fn test_with_data() {
        let data = prepare_test_data();
        assert_eq!(data.len(), 5);
    }

    #[test]
    fn another_test() {
        let data = prepare_test_data();
        assert!(data.contains(&3));
    }
}

Почему это хорошо:

  • Функция не попадает в production-бинарник.
  • Нет предупреждений.
  • Код чище: тестовые утилиты не смешиваются с логикой приложения.

Способ 2: Использовать #[cfg(test)] на самой функции

Если функция должна быть доступна и из основного кода, и из тестов (редкий случай), можно пометить её условной компиляцией:

// Функция будет существовать только при сборке с тестами
#[cfg(test)]
fn prepare_test_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_with_data() {
        let data = prepare_test_data();
        assert_eq!(data.len(), 5);
    }
}

Теперь при cargo build функция просто не существует, и предупреждения нет. При cargo test она компилируется и используется.

Способ 3: Атрибут #[allow(dead_code)] (для быстрых фиксов)

Если вы пишете прототип или функция временная, можно просто подавить предупреждение. Но это считается менее идиоматичным, так как вы скрываете проблему, а не решаете её.

#[allow(dead_code)]
fn prepare_test_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

Дополнительный совет: тестовые утилиты в отдельном файле

Для больших проектов создают отдельный модуль тестовых утилит. Например, файл tests/utils.rs:

// tests/utils.rs
pub fn prepare_test_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

Тогда в тестах вы импортируете её как обычный внешний модуль. Файлы в папке tests/ компилируются только при cargo test, поэтому предупреждений не будет.

Вывод: лучший способ — держать тестовые утилиты внутри #[cfg(test)]-модулей. Это соответствует философии Rust: "платите только за то, что используете", и держит production-код чистым.

<<>>

Альтернативный подход: использование атрибута #[cfg_attr]

Если вы не хотите перемещать функцию в тестовый модуль, но считаете #[allow(dead_code)] слишком грубым, можно применить условное подавление предупреждения только для не-тестовой сборки. Это делается с помощью cfg_attr:

#[cfg_attr(not(test), allow(dead_code))]
fn prepare_test_data() -> Vec<i32> {
    vec![1, 2, 3, 4, 5]
}

Как это работает:

  • Атрибут #[cfg_attr(condition, attr)] применяет атрибут attr только если condition истинна.
  • Условие not(test) означает: "если мы НЕ в тестовом режиме".
  • То есть при cargo build (не тесты) добавляется allow(dead_code), и предупреждение исчезает.
  • При cargo test атрибут не применяется, и функция считается используемой — предупреждения тоже нет.

Этот способ полезен, когда:

  • Функция используется и в тестах, и в основном коде, но пока только в тестах (например, вы пишете тесты до реализации).
  • Вы хотите явно документировать, что функция предназначена для тестов, но оставить её в основном модуле.

Однако лично я рекомендую первый способ из основного ответа — перемещение функции в тестовый модуль. Это чище, и новичку в Rust проще понять, что тестовый код не должен смешиваться с production-кодом. А cfg_attr оставьте для более сложных сценариев, когда без него не обойтись.