Тестирование в 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, остальные разбираются в тренажёре.
- Что происходит с примерами кода в /// -комментариях при cargo test?A)Они компилируются и выполняются как doc-тестыB)Они игнорируются: это просто текст документацииC)Они выполняются лишь при сборке документации через cargo docD)Они проверяются только на синтаксис, без запуска
показать ответ и разбор
+A)Они компилируются и выполняются как doc-тесты// разбор: Каждый блок кода в документации превращается в отдельный тест: компилируется и запускается вместе с остальными. Так примеры не устаревают — сменилась сигнатура, и сборка тестов падает. Если пример не должен запускаться, блок помечают no_run или ignore, а строки со скрытой обвязкой прячут решёткой.
- Тесты используют один временный файл и падают через раз. Вероятная причина?A)Порядок тестов случайный, и один затирает данные другого по очередиB)cargo test кэширует результаты и переиспользует старыеC)Временные файлы удаляются сборщиком мусора в произвольный моментD)Тесты идут параллельно в потоках и дерутся за общий ресурс
показать ответ и разбор
+D)Тесты идут параллельно в потоках и дерутся за общий ресурс// разбор: По умолчанию раннер запускает тесты в нескольких потоках, поэтому общий файл, переменная окружения или таблица в базе превращаются в гонку. Лечится изоляцией — уникальные имена, отдельные каталоги, транзакция с откатом. Прогон в один поток (--test-threads=1) годится для диагностики, но прячет проблему, а не решает её.
- Как в Rust заменить зависимость на дублёр в тестах, если рефлексии нет?A)Пометить функцию #[cfg(test)] и переопределить её в тестовом модулеB)Объявить трейт и подставить тестовую реализациюC)Использовать макрос mock! из стандартной библиотекиD)Подменить структуру через transmute в тестовом коде
показать ответ и разбор
+B)Объявить трейт и подставить тестовую реализацию// разбор: Точка подмены закладывается в дизайн: код зависит от трейта Repository, а не от конкретного PostgresRepo. В тестах подставляется структура-заглушка — вручную или через mockall, который генерирует её по трейту. Отсюда практическое следствие: если подменить нечего, значит зависимость зашита жёстко, и это проблема проектирования, а не тестов.
- Что делает атрибут #[should_panic(expected = "деление на ноль")]?A)Перехватывает панику и продолжает выполнение теста дальшеB)Помечает тест как ожидаемо падающий и исключает его из отчётаC)Считает тест успешным при панике с нужным текстомD)Проверяет, что функция возвращает Err с таким текстом
показать ответ и разбор
+C)Считает тест успешным при панике с нужным текстом// разбор: Тест проходит, только если паника случилась, а её сообщение содержит указанную подстроку — без expected подойдёт любая паника, в том числе от опечатки в самом тесте. Для проверки ветки Err атрибут не нужен: там просто разбирают Result через assert. Тест может и сам возвращать Result, тогда Err считается провалом.
- Почему println! в тесте ничего не печатает, когда тест проходит?A)Вывод уходит в отдельный файл, который создаётся в каталоге targetB)Макросы печати отключены в тестовой сборке ради скорости прогонаC)Раннер перехватывает вывод и показывает его только у упавших тестовD)Печать работает только в тестах каталога tests, а в модульных отключена
показать ответ и разбор
+C)Раннер перехватывает вывод и показывает его только у упавших тестов// разбор: По умолчанию вывод захватывается, чтобы отчёт оставался читаемым: у зелёных тестов он отбрасывается, у упавших печатается. Увидеть его целиком можно флагом --nocapture (или --show-output). Заодно это объясняет, почему при отладке теста удобнее сразу запускать один тест по имени: меньше шума и не мешает параллельный прогон.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.