сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Экосистема Rust

Тестирование в Rust

Тестирование

Спрашивают, где живут тесты, что такое doc-тесты и как подменять зависимости без рефлексии. Последний вопрос - фактически про архитектуру, поэтому отвечать на него надо через дизайн, а не через библиотеки.

Стержень: юнит-тесты видят внутренности, интеграционные смотрят снаружи, doc-тесты держат документацию честной, а подмена строится на трейтах.

// Формулировки: «чем tests/ отличается от mod tests?», «запускаются ли примеры из документации?», «как замокать зависимость?»

Три категории тестов

Юнит-тесты живут в модуле #[cfg(test)] внутри файла: они видят приватные функции через use super::*, а благодаря атрибуту не попадают в релизную сборку. Интеграционные лежат в каталоге tests/ и компилируются отдельными крейтами - им доступно только публичное API, ровно как будущему пользователю.

Третья категория - doc-тесты: примеры кода в /// -комментариях компилируются и выполняются при cargo test. Это спасает документацию от устаревания: поменялась сигнатура - прогон падает. Пометки no_run и ignore меняют поведение, если пример не должен выполняться.

// Проверено: юнит-тест видит приватную функцию, а попытка обратиться к ней из tests/ даёт E0603 - private function. Разделение реальное, а не декоративное.

cfg(test)
модуль существует только в тестовой сборке
doc-тест
пример из документации, выполняемый при cargo test

Параллельность и подмена зависимостей

Раннер запускает тесты в нескольких потоках, поэтому общий временный файл, переменная окружения или таблица в базе превращаются в гонку - тесты падают через раз. Правильное лечение это изоляция: уникальные имена, свой каталог на тест, транзакция с откатом. Флаг --test-threads=1 годится для диагностики, но прячет проблему.

Рефлексии в Rust нет, поэтому подменить зависимость на лету нельзя. Точку подмены закладывают в дизайн: код зависит от трейта Repository, а не от конкретного PostgresRepo. В тестах подставляется структура-заглушка - руками или через mockall, который генерирует её по трейту.

// Побочная польза: если подменить нечего, значит зависимость зашита жёстко. Это проблема архитектуры, которую тесты просто вскрыли.

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn works() { assert_eq!(add(2, 2), 4); }
}
изоляция тестов
отсутствие общих ресурсов между параллельными тестами
mockall
генератор тестовых дублёров по трейту

Как отвечать: «Как подменить зависимость в тестах, если рефлексии нет?»

Через трейты - точка подмены закладывается в дизайн. Код зависит не от конкретного PostgresRepo, а от трейта Repository, и в него можно подставить либо настоящую реализацию, либо тестовую заглушку. Подставлять можно дженериком, если тип известен на этапе компиляции, или через Box<dyn Repository>, если нужна динамика. Заглушку пишу руками, когда она простая, или генерирую через mockall с проверкой ожиданий. И тут есть полезный побочный эффект: если подменять нечего, значит зависимость зашита жёстко это сигнал переработать дизайн, а не искать хитрую библиотеку.

Ты отвечаешь через архитектуру, а не через инструмент, и добавляешь критерий выбора между дженериком и dyn. Это ответ уровня senior.

На чём валятся

  • Ждут, что тесты в tests/ увидят приватные функции.
  • Не знают про doc-тесты и удивляются, что примеры из документации падают в CI.
  • Пишут тесты с общим временным файлом и ловят плавающие падения.
  • Ставят --test-threads=1 вместо изоляции.
  • Ищут библиотеку для моков вместо того, чтобы завести трейт.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.

  1. #rs_testing1 / 5
    Что происходит с примерами кода в /// -комментариях при cargo test?
    A)Они компилируются и выполняются как doc-тесты
    B)Они игнорируются: это просто текст документации
    C)Они выполняются лишь при сборке документации через cargo doc
    D)Они проверяются только на синтаксис, без запуска
    показать ответ и разбор
    +A)Они компилируются и выполняются как doc-тесты

    // разбор: Каждый блок кода в документации превращается в отдельный тест: компилируется и запускается вместе с остальными. Так примеры не устаревают — сменилась сигнатура, и сборка тестов падает. Если пример не должен запускаться, блок помечают no_run или ignore, а строки со скрытой обвязкой прячут решёткой.

  2. #rs_testing2 / 5
    Тесты используют один временный файл и падают через раз. Вероятная причина?
    A)Порядок тестов случайный, и один затирает данные другого по очереди
    B)cargo test кэширует результаты и переиспользует старые
    C)Временные файлы удаляются сборщиком мусора в произвольный момент
    D)Тесты идут параллельно в потоках и дерутся за общий ресурс
    показать ответ и разбор
    +D)Тесты идут параллельно в потоках и дерутся за общий ресурс

    // разбор: По умолчанию раннер запускает тесты в нескольких потоках, поэтому общий файл, переменная окружения или таблица в базе превращаются в гонку. Лечится изоляцией — уникальные имена, отдельные каталоги, транзакция с откатом. Прогон в один поток (--test-threads=1) годится для диагностики, но прячет проблему, а не решает её.

  3. #rs_testing3 / 5
    Как в Rust заменить зависимость на дублёр в тестах, если рефлексии нет?
    A)Пометить функцию #[cfg(test)] и переопределить её в тестовом модуле
    B)Объявить трейт и подставить тестовую реализацию
    C)Использовать макрос mock! из стандартной библиотеки
    D)Подменить структуру через transmute в тестовом коде
    показать ответ и разбор
    +B)Объявить трейт и подставить тестовую реализацию

    // разбор: Точка подмены закладывается в дизайн: код зависит от трейта Repository, а не от конкретного PostgresRepo. В тестах подставляется структура-заглушка — вручную или через mockall, который генерирует её по трейту. Отсюда практическое следствие: если подменить нечего, значит зависимость зашита жёстко, и это проблема проектирования, а не тестов.

  4. #rs_testing4 / 5
    Что делает атрибут #[should_panic(expected = "деление на ноль")]?
    A)Перехватывает панику и продолжает выполнение теста дальше
    B)Помечает тест как ожидаемо падающий и исключает его из отчёта
    C)Считает тест успешным при панике с нужным текстом
    D)Проверяет, что функция возвращает Err с таким текстом
    показать ответ и разбор
    +C)Считает тест успешным при панике с нужным текстом

    // разбор: Тест проходит, только если паника случилась, а её сообщение содержит указанную подстроку — без expected подойдёт любая паника, в том числе от опечатки в самом тесте. Для проверки ветки Err атрибут не нужен: там просто разбирают Result через assert. Тест может и сам возвращать Result, тогда Err считается провалом.

  5. #rs_testing5 / 5
    Почему println! в тесте ничего не печатает, когда тест проходит?
    A)Вывод уходит в отдельный файл, который создаётся в каталоге target
    B)Макросы печати отключены в тестовой сборке ради скорости прогона
    C)Раннер перехватывает вывод и показывает его только у упавших тестов
    D)Печать работает только в тестах каталога tests, а в модульных отключена
    показать ответ и разбор
    +C)Раннер перехватывает вывод и показывает его только у упавших тестов

    // разбор: По умолчанию вывод захватывается, чтобы отчёт оставался читаемым: у зелёных тестов он отбрасывается, у упавших печатается. Увидеть его целиком можно флагом --nocapture (или --show-output). Заодно это объясняет, почему при отладке теста удобнее сразу запускать один тест по имени: меньше шума и не мешает параллельный прогон.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.