Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Тема
Описание
Доп.

Советы по тестированию

Автоматическое тестирование - это система безопасности которая защищает программу от ее программистов (защитой от его когнитивных и человеческих ошибок.)

Когда другой программист вносит изменения которые ломают твой метод и он при этом ничего не знает про твой метод, так как тесты не запускались что бы проверить все взаимосвязи.

Тестирование — это процесс попытки заставить программу провалиться (Glenford J. Myers)

Тестирование — это систематические и настойчивые попытки сломить сопротивление нормально работающей программы и заставить ее сделать ошибку.

Работа тестировщика состоит в том что бы доказать, что ПО не работает. А не наоборот, в том что бы убедиться, что в ПО нет ошибок. Код нельзя навать завершенным и готовым к выпуску, если в нем не было найдено ошибок. Решение о выпуске принимают на основании спада количества обнаруженных ошибок до приемлемого уровня, так как задача найти ошибку раньше пользователя выполнена.

Процитируем рассказ Дональда Кнута (Donald Knuth) о создании тестов для системы форматирования текстов ТеХ: "Я постарался выработать в себе самый подлый и изощренно коварный настрой, на который только был способен, а затем написал самый гнусный (тестирующий) код, какой только смог придумать; потом я поместил его в еще более извращенные конструкции, которые находились уже на самой грани неприличия". Напомним, что цель тестирования — не продемонстрировать, что программа работает хорошо, а найти в ней ошибки.

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

Такой подход к тестированию похож на тестирование на проникновение, когда целенаправленно ищут уязвимости, это очень близкие идеи, но не тождественны. Майерс про тестирование говорит: тестировщик должен пытаться сломать ПО, а пентестёр (penetration tester) делает то же самое, но с акцентом на безопасность и эксплуатацию уязвимостей.

Еще одна сторона подхода тестирования, убедится что код работает так как ожидалось - это минимум, но нужно еще протестировать граничные случаи, комбинации …, пустые или большие строки….

Сходства penetration tester и тестировщика:

  • Менталитет «атакующего»: обе роли думают не «как всё работает», а «как это можно заставить не работать».
  • Цель — найти слабые места, показать, где система падает или ведёт себя неправильно.
  • Методы частично пересекаются: негативное тестирование (negative testing), fuzzing, fault injection, boundary/edge-case tests, стресс-тесты, сценарии с неверными данными.

Главное различие:

  • Тестирование по Майерсу — широкое понятие: ищут баги функциональные, логические, удобства использования, регрессии, ошибки в обработке краёв и т.п.
  • Пентест — узкая специализация: ищут безопасностные уязвимости (XSS, SQLi, RCE, privilege escalation, CSRF, auth bypass и т. п.), оценивают возможность эксплуатации, способность атакующего получить доступ или выполнить нерегламентированные действия.

Проще: тестировщик пытается «сломать» систему как пользователь/интегратор, пентестёр пытается «взломать» её как злоумышленник.

Области фокуса

  • Функциональное тестирование / негативное тестирование: валидация входных данных, границы, race conditions, ошибочные состояния, recoverability.
  • Security testing / pentesting: авторизация/аутентификация, контроль доступа, инъекции, криптография, утечки конфиденциальных данных, сетевые векторы, цепочки доверия.

Ошибочно думать что задача тестировщиков - это убедиться в высоком качестве ПО. И подтверждении, что все функции работают как ожидается.

Тестирование это процесс:

  • проведения экспериментов
  • сравнения фактических результатов с ожидаемыми
  • документированная наблюдаемых несоответствий

А обеспечения качества (QA) это более широкая область включающая в себя тестирования и им не занимается отдел тестирования, а занимается специалисты по обеспечению качества.

  • Запускайте все тесты в CI при каждом изменении (за исключением Fuzzy Testing).
  • Когда вы исправляете ошибку, напишите тест, который выявляет ошибку, прежде чем ее исправлять. Таким образом, вы можете быть уверены, что ошибка исправлена ​​и не возникнет случайно в будущем. (Если ошибка появилась, значит вы ошиблись с покрытием тестами, и следует покрыть эту зону тестами)
  • Если ваш ящик имеет features, проведите испытания для всех возможных комбинаций доступных features.
  • В более общем случае, если ваш контейнер включает какой-либо специфичный для конфигурации код (например, #[cfg(target_os = "windows")]), запустите тесты для каждой платформы , на которой есть отдельный код.

Напишите модульные тесты для комплексного тестирования, включающего тестирование только внутреннего кода. Запустите их с помощью cargo test.

Напишите интеграционные тесты для проверки вашего публичного API. Запустите их с помощью cargo test.

Напишите тесты документации, которые иллюстрируют, как использовать отдельные элементы в вашем публичном API. Запустите их с помощью cargo test.

Напишите примеры программ, которые показывают, как использовать ваш публичный API в целом. Запустите их с помощью $cargo test --examples или cargo run --example <name>.

Напишите бенчмарки, если ваш код имеет существенные требования к производительности. Запустите их с помощью cargo bench.

Напишите fuzz-тесты, если ваш код подвергается воздействию ненадежных входных данных. Запускайте их (непрерывно) с помощью cargo fuzz

Эффективные тесты ясны, лаконичны и направлены на проверку определенного поведения вашего кода, включая нормальные условия, граничные случаи и потенциальные ошибки. Они должны быть понятны другим разработчикам, давая им понять, что тестируется и почему.

Помните, никто не знает ваш код лучше вас, поэтому особенно важно тестировать случаи, которые, как вы знаете, могут быть упущены во время ручного или автоматизированного тестирования.

Этапы Конвейера (Pipeline Stages)

Этапы Конвейера (Pipeline Stages)

ШагДействие (Action)Ветки (Branches)Назначение (Purpose)
1.Слияние рабочей ветки в devtaskdevИнтеграция и тестирование новых ф-й и исправлений в изолированной среде. Запуск Unit- и Integration-тестов.
2.Слияние dev в masterdevmasterГотовность к релизу. master теперь содержит все стабильные, протестированные ф-и, готовые к следующему релизу. CI/CD проверки.
3.Слияние master в releasemasterreleaseНачало цикла стабилизации. release создается для подготовки к развертыванию, здесь вносятся только критические исправления (hotfixes).
4.Стабилизация и РазвертываниеreleaseProductionФинальное тестирование, стабилизация, сборка артефактов и развертывание в производственной среде (production).

Проверяйте граничные условия и предельные случаи.

Тестирование методом "черного ящика" означает, что тестирующий ничего не знает о внутреннем устройстве кода и не имеет доступа к исходному тексту. Начинать тестирование "черного ящика" лучше всего с предельных случаев, а затем переходить к более тяжелым тестам: исходным данным большого объема, искаженным и неожиданным данным и, наконец, некорректным данным, которые программа не должна обрабатывать. Конечно, чтобы убедиться в правильности работы "основной линии" программы, следует задавать и среднестатистические, обычные исходные данные.

Составьте список граничных случаев и по возможности напишите для них автоматизированные тесты или же выполните ручное тестирование.

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

Проверяйте граничные условия и предельные случаи. операторов — проверяйте на месте, разветвляется ли выполнение по правильному пути и выполняется ли цикл нужное количество раз. Этот процесс называется проверкой граничных условий (или предельных случаев) по той причине, что контроль правильности выполняется на естественных границах кода и данных.

Проверяйте предусловия и постусловия. Еще один способ профилактики ошибок состоит в том, чтобы убедиться, что некоторые ожидаемые или необходимые условия выполняются до (предусловия) и после (постусловия) определенного фрагмента кода.

Например функция avg для вычисления среднего арифметического п элементов массива работает неправильно, если n меньше или равно нулю.

Используйте контрольные условия. В языках С и C++ имеются средства для проверки контрольных предусловий и постусловий, объявленные в заголовочном файле <assert.h>. Если контрольное условие не выполняется, программа завершается аварийно, поэтому такие условия приберегают для ситуаций действительно неожиданных, в которых причина краха неизвестна, а дальнейшая работа программы невозможна.

Предусматривайте все случаи. Хорошей практикой следует считать добавление в программу кода на случай ситуаций, которые "не могут произойти". Здесь имеется в виду, что некоторые ошибки никак не могут случиться по вине программы, однако все же возникают в силу посторонних непредсказуемых обстоятельств.

Автоматическое регрессионное тестирование. (регрессионные тесты - это тесты написанные ранее по отношению к новым тестам). Начинать автоматизацию следует с регрессионного тестирования — последовательности тестов, сравнивающих новую версию какой-либо программы с ее же предыдущей версией. Назначение регрессионного тестирования состоит как раз в том, чтобы гарантировать неизменную работу всех функций программы, кроме добавленных.

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

Нагрузочное тестирование

Например, компания в сфере интернет-торговли знает, что в «черную пятницу» ее трафик увеличится примерно в 10 раз, и поэтому хочет проверить, будет ли задержка ответа серверов допустимой.

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

Бывает весьма поучительно (и иногда отрезвляюще) подвергнуть ничего не подозревающую программу, которая ожидает текстовых данных, стрессу в виде двоичного входного потока, например кода скомпилированной программы. Хорошие тестовые примеры часто можно применить сразу к нескольким программам. Например, всякую программу, считывающую файлы, следует тестировать на пустом файле. Любую программу, читающую и обрабатывающую текстовые строки, необходимо тестировать на очень длинных и пустых строках, а также на входном потоке, вообще не разделенном на строки.

Тестируйте программу на нескольких аппаратных платформах, в нескольких выполняющих средах или операционных системах; скомпилируйте ее несколькими разными компиляторами. Такие сочетания позволяют обнаружить особенности, которые не видны другими способами, например, зависимость от порядка байтов в слове и длины целых чисел, восприятие нулевых указателей, символов перевода строки и возврата каретки, специфические свойства библиотек и заголовочных файлов.

Данный тип тестирования характерен исключительно для бэкенда, и его можно проводить несколькими способами:

  • Специальная тестовая инфраструктура, отправляющая запросы к тестируемым системам. В этом случае настраивается инфраструктура, генерирующая запросы.

  • Пакетирование существующих производственных запросов. В данном случае производственные запросы преднамеренно задерживаются, а после накопления достаточного их количества они отправляются на системы. Такой подход работает с запросами, не зависящими от времени.

  • Тестирование на производственных системах с меньшей инфраструктурой. Вместо тестирования текущей инфраструктуры на возможность выдержать 10-кратное увеличение трафика проводится тест, проверяющий, выдержит ли 1/10 инфраструктуры текущие объемы трафика.

Хаос-тестирование

Chaos Monkey («Обезьяна хаоса»). Задача у нее была следующая — случайным образом отключать инстансы и сервисы в нашей архитектуре. Если мы не будем постоянно проверять нашу способность преодолевать сбои, то в случае неожиданного отказа система, скорее всего, не выдержит.

С тех пор как Netflix сделал исходный код Chaos Monkey открытым, система обрела популярность среди компаний, управляющих большим количеством сервисов. Команды инфраструктуры используют такой подход намеренного отключения сервисов, чтобы понаблюдать за реакцией системы.

Конкретные техники «как ломать»

Конкретные техники «как ломать» (подходы тестировщика и пентестёра)

  • Негативное тестирование: неправильные типы, очень длинные строки, пустые значения, вложенные структуры, неверные границы.
  • Граничные состояния: min/max, off-by-one, переполнение буфера, округления.
  • Fuzzing: случайные/мутирующие входы, генерация некорректных бинарных/текстовых форматов.
  • Mutation testing: изменять код/входы, чтобы проверить, ловят ли тесты изменения.
  • Fault injection: имитация отказов сети, сбой сервисов, нехватка памяти/IO.
  • Сценарии злоумышленника: эскалация прав, обход аутентификации, CSRF/XSS/SQLi, манипуляция токенами.
  • Replay атак: повторные запросы с изменёнными параметрами/таймингом.

Отрицательные тесты — bad/unhappy path

Это тесты, которые проверяют, что функция правильно реагирует на ошибки, некорректные данные, нештатные ситуации.

Типичные примеры:

  • передаёшь NULL → функция должна вернуть ошибку или не упасть
  • передаёшь слишком маленький буфер → функция должна корректно сообщить об этом
  • вводишь неверный формат → парсер должен отказать
  • файл не существует → корректная ошибка, а не segmentation fault
  • переполнение, выход за границы, неправильные параметры

Стиль именования тестовых функций

Стиль именования тестовых функций

test_add_should_return_sum_of_two_numbers

  • test - префикс чтобы сразу понимать: это тест, а не обычная функция.
  • add - имя тестируемой функций или модуля
  • should_return_sum_of_two_numbers - пишут ожидаемое поведение функции
    • test_parser_should_fail_on_invalid_input
    • test_queue_should_be_empty_initially

Минимальный стиль

test_add_invalid_json

баг-трекинг

(Glenford J. Myers)

Общая идея Майерса (автора книги The Art of Software Testing (1979), одной из первых системных работ о тестировании программ)

Майерс хотел донести, что ошибка — это не просто факт, а объект управления качеством.

Эффективная команда должна не просто фиксить баги, а учиться на них, улучшая архитектуру, тесты и требования.

Он писал, что ошибка должна быть:

“идентифицируема, воспроизводима и управляемая”.

Для этого у каждой ошибки (в баг-трекере или отчёте) должны быть атрибуты — это не просто формальность, а способ превращения хаоса в управляемый процесс.

Её нужно описывать формально, чтобы можно было:

  1. Анализировать причины (root cause analysis),
  2. Собирать статистику по типам ошибок,
  3. Улучшать процессы (профилактика ошибок, а не только исправление).

Современная интерпретация

Его идеи легли в основу всех современных систем баг-трекинга — JIRA, Bugzilla, Redmine и т. д.

Там ты видишь именно эти поля:

  • Reporter / Assignee (автор, исполнитель),
  • Severity, Priority,
  • Description, Steps to reproduce,
  • History / Comments.

Основные атрибуты ошибки по Майерсу

АтрибутЗначение
Автор (Originator)Кто обнаружил ошибку. Это важно, чтобы при необходимости уточнить контекст, шаги воспроизведения, или понять, где она была найдена — в тестировании, эксплуатации и т. д.
Симптомы (Symptoms)Как проявляется ошибка: что видит пользователь, какие сообщения, при каких условиях. Это помогает воспроизводить и диагностировать проблему.
История изменений (History / Status tracking)Как менялась ошибка со временем — когда создана, кому назначена, исправлена ли, повторялась ли. Это даёт прозрачность в управлении качеством.
Серьёзность (Severity)Насколько ошибка критична с точки зрения системы (например: не работает ядро приложения vs. незначительная ошибка отображения).
Приоритет (Priority)В каком порядке её нужно исправлять. Ошибка может быть не очень серьёзной, но при этом мешать тестированию других функций, поэтому приоритет высокий.

Сайт Codecov (app.codecov.io) — это платформа для отчетности и анализа покрытия кода тестами (Code Coverage)

Она интегрируется с вашими системами непрерывной интеграции (CI) и репозиториями (GitHub, GitLab, Bitbucket), чтобы предоставить разработчикам и командам глубокое понимание того, насколько полно их код протестирован.

Validation Testing (Верификация в тестировании ПО) – процесс просмотра документации, дизайна, кода и программы для того, чтобы проверить, было ли программное обеспечение создано в соответствии с требованиями или нет.

Function Testing (Функциональное тестирование API) - проверяет отдельные функции API, отправляя различные входные данные, чтобы гарантировать правильную функциональность и ожидаемые ответы, а также надежность перед развертыванием.

UI Testing (Ul-тестирование) - проверяет пользовательский интерфейс, проверяя взаимодействие между пользовательским интерфейсом и API, обеспечивая точность синхронизации и отображения данных. Он выявляет проблемы, связанные с поиском, отображением данных и взаимодействием с пользователем.

Load Testing (Нагрузочное тестирование) - оценивает производительность приложения при различных требованиях пользователей путем моделирования интенсивного одновременного использования для выявления потенциальных проблем и обеспечения надежной работы во время пикового трафика.

Runtime/Error Detection - Обнаружение ошибок во время выполнения при тестировании API включает в себя активный мониторинг и анализ поведения приложения во время выполнения для выявления и фиксации ошибок или аномалий, которые могут возникнуть при обработке запросов API.

Security Testing - Тестирование безопасности при тестировании API проверяет уязвимость приложения к потенциальным рискам и угрозам безопасности. Он включает в себя оценку API на предмет потенциальных лазеек в безопасности, таких как несанкционированный доступ, утечка данных и другие уязвимости.

Penetration Testing - Тестирование на проникновение при тестировании API включает в себя оценку безопасности API приложения путем попытки использования уязвимостей и слабых мест. Цель состоит в том, чтобы защитить API от потенциальных угроз и обеспечить наличие надежных мер безопасности.

Fuzzy Testing - Нечеткое тестирование при тестировании API включает автоматическую отправку большого количества случайных, недействительных или неожиданных входных данных в API для выявления уязвимостей и потенциальных ошибок.

Interoperability and WS Compliance Testing - Тестирование совместимости обеспечивает бесперебойную связь между приложением и различными платформами. Тестирование на соответствие требованиям WS проверяет соответствие API стандартам веб-служб для обеспечения надежного взаимодействия.

Автоматизированные A/B тесты

Автоматизированные A/B тесты — это маркетинговый и продуктовый инструмент для принятия решений.

Автоматизированные A/B тесты — это экспериментальный статистический метод, который относится к тестированию юзабилити, маркетинга и продукта и фокусируется на поведении пользователей.

Цель: Сравнить две или более версии (А и Б) элемента продукта (например, кнопка, заголовок, дизайн страницы) для определения, какая из них лучше достигает бизнес-целей (например, увеличивает конверсию, кликабельность, доход).

Объект тестирования: Пользовательский опыт (UX) и метрики эффективности (конверсия, время на сайте, отказы) при взаимодействии с разными версиями.

Методология: Автоматизированное распределение реальных пользователей по группам, показ им разных версий продукта и статистический анализ собранных данных о поведении.

Результат: Статистически значимое заключение о том, какая версия (А или Б) является более эффективной с точки зрения бизнес-метрик.

Когда используются: После запуска продукта (или его части) для оптимизации и принятия продуктовых решений на основе данных о реальном поведении пользователей.

Flaky tests

Самая раздражающая трата времени инженера — это flaky test

Flaky tests — это тесты, которые иногда проходят успешно, а иногда падают, без каких-либо изменений в коде или окружении. Они ведут себя непредсказуемо и ненадежно, что затрудняет определение реального состояния кода. Основная проблема с flaky тестами заключается в том, что они могут создавать ложное ощущение того, что в коде есть баги, хотя фактически ошибки могут быть связаны с самим тестом или окружением, в котором он выполняется.

Причины появления flaky tests:

  1. Асинхронность или параллелизм: Тесты могут зависеть от параллельных процессов или асинхронных операций, что приводит к случайным сбоям из-за некорректного порядка выполнения кода.
  2. Зависимость от времени: Тесты, которые проверяют выполнение кода в течение определенного времени, могут быть чувствительны к задержкам, нагрузкам на систему или изменению времени выполнения операций.
  3. Зависимость от внешних систем: Если тесты взаимодействуют с внешними системами, такими как базы данных, веб-сервисы или API, они могут стать нестабильными из-за проблем с сетью, перегрузок или нестабильности этих внешних систем.
  4. Недетерминированное поведение: Тесты могут быть написаны таким образом, что их результат зависит от случайных факторов или глобального состояния программы (например, использования случайных чисел или глобальных переменных).
  5. Сетевые или инфраструктурные проблемы: В случае выполнения тестов в распределенных системах или в облачных средах могут возникать временные проблемы с сетью или доступом к ресурсам.

Проблемы с flaky tests:

  • Недоверие к тестам: Разработчики могут перестать доверять автоматическим тестам, если они часто выдают ложные срабатывания (false negatives).
  • Трудности с CI/CD: Flaky тесты могут препятствовать непрерывной интеграции (CI) и деплою (CD), так как падающие тесты могут блокировать процессы выпуска новых версий.
  • Сложность отладки: Трудно определить причину падения flaky теста, так как его поведение непредсказуемо и не воспроизводится при каждом запуске.

Как бороться с flaky tests:

  1. Локализовать и устранять причину нестабильности: При выявлении flaky теста стоит попытаться понять, от чего зависит его нестабильное поведение, и устранить эти зависимости.
  2. Переписать тесты: В некоторых случаях переписывание теста так, чтобы он не зависел от асинхронных операций или случайных факторов, может решить проблему.
  3. Изолировать flaky tests: Если причина не может быть устранена сразу, можно временно изолировать flaky тесты и пометить их соответствующим образом, чтобы они не блокировали основной процесс сборки.
  4. Использовать ретраи (повторные запуски): Иногда повторные запуски тестов (ретраи) могут помочь обойти временные проблемы с flaky тестами. Это временное решение, но оно может сократить количество ложных срабатываний.

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

Snapshot тесты — это тесты, которые делают скриншот экрана (эталонный скриншот) и сравнивают с актуальным скриншотом, который делается во время прогона тестов. Делать Snapshot тесты на все возможные проверки — плохая практика, так как если будет редизайн, ваши тесты моментально станут красными, и вам придется в срочном порядке менять эталонные скриншоты. Я советую использовать Snapshot тесты для проверки верстки, элементов, с которыми тяжело взаимодействовать по accessibilityidentifier (график цен, календарь и прочие).

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

crate insta предназначен для снимочного тестирования (snapshot testing), где текущее состояние данных сравнивается с ранее сохранённым снимком. Это полезно для тестирования сложных выходных данных, таких как структуры JSON, HTML или сообщения об ошибках.

Особенности:

  • Автоматическое создание и обновление снимков.
  • Поддержка встроенных снимков (inline snapshots), где данные хранятся в коде.
  • Удобный инструмент для просмотра изменений в снимках (cargo insta review).
  • Интеграция с CI/CD пайплайнами для предотвращения нежелательных изменений в выходных данных.

Тестирование свойств дает возможность исследовать случайную выборку из предопределенного пространства тестирования путем установки «свойств» тестовых данных.

Тесты свойств могут помочь найти тестовые входные данные, которые не проходят ваши тесты, но они не могут проверить все входные данные из-за ограниченного пространства для исследования. Если тест не пройден, крейт тестирования свойств Rust проходит процесс, называемый сжатием

Он позволяет проверить, что определенные свойства вашего кода сохраняются для произвольных входных данных, и, если обнаружен сбой, автоматически находит минимальный тестовый пример для воспроизведения проблемы. В отличие от QuickCheck, создание и сжатие определяется для каждого значения, а не для каждого типа, что делает его более гибким и упрощает композицию.

QuickCheck и Proptest во многом схожи: оба генерируют случайные входные данные для функции для проверки определенных свойств и автоматически сокращают входные данные до минимального количества отказов.

Одно большое отличие состоит в том, что QuickCheck генерирует и сжимает значения только на основе типа, тогда как Proptest использует явные Strategy объекты.

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

Представьте себе мишень для дартс. Традиционные модульные тесты подобны стрельбе в яблочко тщательно выбранной стрелой. Напротив, тестирование на основе свойств больше похоже на выпуск рая пчел на мишень и проверку, все ли они достигли допустимых целей.


Цель здесь состоит в том, чтобы наша add функция могла обрабатывать любые странные и замечательные строки proptest без сбоев

[dev-dependencies]
proptest = "0.9"

use proptest::prelude::*;

proptest! {
    #[test]
    fn doesnt_crash(s in "\\PC*") {
        adder::add(s);
    }
}

Результат в файл /proptest-regressions/*.txt следует добавить папку в gitignore

Fuzzing генерация не правильных данных

  • cargo afl
  • Фаззинг
  • Rust Fuzz Book
  • crate cargo-fuzz
  • crate honggfuzz
  • effective-rust/fuzz-testing
  • how-to-organize-rust-tests
  • youtube/cargo afl
  • complete-guide-to-testing-code-in-rust
  • Cargo Fuzz - Обертка командной строки для использования libFuzzer. Прост в использовании, не нужно перекомпилировать LLVM!
  • honggfuzz-rs - Фаззер, разработанный Google.
  • afl.rs - Позволяет запускать фаззер AFL для кода, написанного на языке программирования Rust.
  • cargo-libafl - Фаззер, поддерживаемый LibAFL
  • fuzzcheck - на наличие нечеткости, экспериментальный движок фаззинга, который напрямую мутирует структуры данных Rust, минуя преобразование в/из байтовых строк
  • QuickCheck - это способ проведения тестирования на основе свойств с использованием случайно сгенерированных входных данных.
  • Proptest - это фреймворк для тестирования свойств (семейство QuickCheck), созданный на основе фреймворка Hypothesis для Python.
  • rusty-radamsa - Radamsa портирован на Rust. Fuzzer с хорошими мутаторами, но без руководства по покрытию.

Фаззинг (англ. fuzzing) — техника тестирования программного обеспечения, часто автоматическая или полуавтоматическая, заключающаяся в передаче приложению на вход неправильных, неожиданных или случайных данных.

Цель фаззинга — завершить работу программы с различными входными данными, которые разработчики и тестировщики, возможно, не учли при написании тестов

Тестирование на баги, рандомные данные подаются на ваш вход Поиск свойства которое не работает т.е. мы не знаем свойства как при Property based testing, мы его ищем для всех случаев

Нечеткое тестирование не поможет заставить программы работать должным образом, но поможет обнаружить источники сбоев программ

На данный момент в экосистеме Rust есть несколько инструментов для фаззинга.

Наиболее известны:

  • cargo-fuzz - это оболочка командной строки для использования libFuzzer .
  • afl.rs позволяет запускать AFL (американский fuzzy lop) на коде, написанном на Rust .
  • honggfuzz - это фаззер, ориентированный на безопасность, с мощными возможностями анализа, который поддерживает эволюционный фаззинг с обратной связью, основанный на покрытии кода (программном и аппаратном).

Только fuzz(afl) смог найти вариант (255,255,255) на который я поставил panic!.

В случае с proptest ...(r in u8::MIN..=u8::MAX,g in u8::MIN..=u8::MAX,b in u8::MIN..=u8::MAX) и arbtest не смогли подобрать.

Получается proptest и arbtest не перебирают все варианты комбинаций u8 (u8,u8,u8)


Изолированное фаззинг-инфраструктура: В CI нельзя запускать фаззинг — нестабильные ошибки ≠ регрессии.

Лучший подход:

  • Отдельный сервер постоянно фаззит master так ка CI должен быть чистым и воспроизводимым
  • Найденные входы добавляются в корпус
  • В CI гоняем этот корпус → если он ломается, значит баг в новом коммите

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

Это интеграционные тесты которые более конкретно оценивают всю систему, иначе называемым сквозными тестами.

Вы не просто проверяете правильность своей логики (что можно проверить с помощью модульных тестов), но и то, что ваше программное обеспечение, оборудование, сеть и разрешения работают вместе.

Модульные тесты и локализованное тестирование сценариев использования могут проверить вашу бизнес-логику, но сквозной тест также проверяет инфраструктуру, в которой работает и с которой взаимодействует ваше программное обеспечение.

Для такого тестирования создается тестовое окружение (среда), идентичное окружению, в котором работают реальные пользователи. Тестируются все действия, которые пользователи могут выполнять в приложении.

Тестируется весь user flow (путь пользователя). Например, при разработке онлайн-магазина тестировщик «идет по пути пользователя» от входа посетителя на сайт и регистрации до завершения покупки.

Сквозные тесты самые медленные, потому что время уходит на билд, деплой, запуск приложения

Интеграционные тесты для бинарных крейтов

Если наш проект является бинарным крейтом, который содержит только src/main.rs и не содержит src/lib.rs, мы не сможем создать интеграционные тесты в папке tests и подключить функции определённые в файле src/main.rs в область видимости с помощью инструкции use.

Только библиотечные крейты могут предоставлять функции, которые можно использовать в других крейтах; бинарные крейты предназначены только для самостоятельного запуска.

Это одна из причин, почему проекты на Rust, которые генерируют исполняемые модули, обычно имеют простой файл src/main.rs, который в свою очередь вызывает логику, которая находится в файле src/lib.rs. Используя такую структуру, интеграционные тесты могут проверить библиотечный крейт, используя оператор use для подключения важного функционала. Если этот важный функционал работает, то и небольшое количество кода в файле src/main.rs также будет работать, а значит этот небольшой объём кода не нуждается в проверке.

Как организовать e2e

Файл Cargo.toml:

[features]
dev = []

Файл тестов: tests/e2e_test.rs:

mod e2e;
  ...
let settings = e2e::get_settings()

Файл общих функции: tests/e2e/mod.rs:

pub fn get_settings()...

Запуск:

cargo test --features dev 

Mocking (для Unit тестов)

isahc = { version = "1.6", features = ["json"]}

Тестирует имплементацию т.е. детали реализации, кто кого вызывает и что возвращает. При изменении зависимостей придется переписывать тесты так как от конкретных реализаций зависел тест.

Дает возможность создать под вашим контролем ложную функциональность, имитирующую поведение реальной функциональности. Затем эта функциональность группируется в макет объекта (или просто макет ).

Mocking тестирование — это подход к модульному тестированию, который позволяет вам делать утверждения о том, как тестируемый код взаимодействует с другими модулями системы. При макетном тестировании зависимости заменяются объектами, имитирующими поведение реальных. Цель Mocking — изолировать и сосредоточиться на тестируемом коде, а не на поведении или состоянии внешних зависимостей.

Mockall предоставляет инструменты для создания макетов практически любого признака или структуры.

Наибольший интерес на данный момент вызывает crate mockiato, поскольку он достаточно эргономичен в использовании и поддерживает стабильную версию Rust.

crate unimock - работает очень похоже, но поддерживает супертрейты, поскольку Unimock для насмешек используется один тип.

crate faux и mry - ориентированы на насмешку над структурой (а не над чертами).

Кроме того, следует упомянуть ящики как весьма полезные для HTTP-тестирования mockito, wiremock

Однако самым мощным является crate mockall.

Fake (для Unit тестов)

crate fake для генерации поддельных данных, таких как имя, номер, адрес, лор, даты и т. д.

Предназначен для тестирования поведения кода. Изменение деталей реализации не влияет на тесты, поведение прежнее.

Практика в Rust, обычно используют оба подхода, но для разных целей:

  • Fakes - для репозиториев, внешних сервисов, сложных зависимостей
  • Mocks - для проверки специфических взаимодействий между компонентами

Fake - это рабочая, но упрощенная реализация, обычно для изоляции от внешних зависимостей:

#![allow(unused)]
fn main() {
// Настоящая база данных
struct Database { /* сложная логика */ }

// Fake-версия для тестов
struct FakeDatabase {
    data: HashMap<String, String>,
}

impl FakeDatabase {
    fn new() -> Self {
        Self { data: HashMap::new() }
    }
    
    // Такие же методы как у настоящей БД, но работают в памяти
    fn save(&mut self, key: &str, value: &str) {
        self.data.insert(key.to_string(), value.to_string());
    }
    
    fn load(&self, key: &str) -> Option<&str> {
        self.data.get(key).map(|s| s.as_str())
    }
}
}

Mock - это "шпион", который записывает вызовы и проверяет взаимодействия:

#![allow(unused)]
fn main() {
use mockall::automock;

#[automock]
trait EmailService {
    fn send_email(&self, to: &str, subject: &str) -> Result<(), String>;
}

// В тестах
let mut mock_email = MockEmailService::new();
mock_email.expect_send_email()
    .times(1)
    .returning(|_, _| Ok(()));

// Тестируем, что send_email был вызван ровно 1 раз
}

Miri

Проверка заимствований в небезопасной среде

Экспериментальный интерпретатор промежуточного представления среднего уровня Rust (MIR). Он может запускать двоичные файлы и наборы тестов грузовых проектов и обнаруживать определенные классы неопределенного поведения, например:

  • Доступ к памяти за пределами памяти и использование после освобождения
  • Недопустимое использование неинициализированных данных
  • Нарушение внутренних предварительных условий (достижение unreachable_unchecked, вызов copy_nonoverlapping с перекрывающимися диапазонами, ...)
  • Недостаточно выровненные обращения к памяти и ссылки
  • Нарушение некоторых инвариантов базового типа (например, логическое значение, отличное от 0 или 1, или недопустимый дискриминант перечисления)

Экспериментальный вариант:

  • нарушения правил Stacked Borrows,
  • регулирующих использование псевдонимов для ссылочных типов.

Экспериментально:

  • гонки данных (но без слабых эффектов памяти)

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


Однако имейте в виду, что Miri не будет обнаруживать все случаи неопределенного поведения в вашей программе и не может запускать все программы.

rustup +nightly component add miri

cargo +nightly miri test

Как безопасно тестировать в продакшене?

Вот несколько подходов:

  • Фича-флаги. Чтобы протестировать новую функциональность в продакшене, поместите ее за фича-флагом. После развертывания включите фича-флаг для выполнения автоматизированных и ручных тестов. А когда команда будет уверена, что функционал работает, его можно постепенно разворачивать уже для большего числа пользователей. Подробнее о фича-флагах мы поговорим в части IV, в главе 17 «Выпуск в продакшен».

  • Канареечное развертывание. Внедряйте изменения в продакшен для небольшого числа серверов или пользователей (группы «канареек»). Запустите тесты и отслеживайте результаты. Если никаких проблем не обнаружено, продолжайте развертывание для всех пользователей и серверов.

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

  • Автоматический откат — комбинация канареечного или сине-зеленого развертывания с автоматизированной системой мониторинга. Если система обнаружит какую-либо проблему при развертывании функционала, изменения автоматически откатываются, позволяя команде исследовать проблему.

  • Мультиарендные среды. Идея мультиарендности (иначе называется «мультитенантность») заключается в том, что контекст передается с запросом, а сервисы, его принимающие, уже определяют, является ли он продакшен-запросом, тестовой арендой, бета-арендой или каким-то другим запросом. Сервисы имеют встроенную логику для поддержки аренды и могут обрабатывать или маршрутизировать запросы по-разному.

Соблюдение нормативных требований и юридических ограничений.

Тестирование в продакшене не означает, что инженеры получают доступ к конфиденциальным данным пользователей, таким как персональные данные (PII, personally identifiable information), поэтому может потребоваться создание специальных инструментов для соблюдения требований конфиденциальности при отладке и тестировании.

Термин происходит от фразы «канарейка в угольной шахте» — практики шахтеров, которые брали с собой в шахту клетку с канарейкой, чтобы обнаружить наличие опасного газа. Птица имеет меньшую терпимость к токсичным газам, чем человек, поэтому если канарейка переставала петь или падала замертво, это служило предупреждением о наличии газа, и шахтеры покидали шахту.

Сегодня канареечное тестирование означает развертывание изменений кода для небольшой части пользователей и мониторинг показателей этого процесса на предмет возможных проблем. Распространенный способ реализации канареечного тестирования — маршрутизация трафика на новую версию кода с использованием балансировщика нагрузки или развертывание новой версии кода на одном узле.

BDD стиль

BDD требует, чтобы имена тестов были целыми предложениями, которые начинаются с глагола в сослагательном наклонении и следовали бизнес целям.

тесты для некоторой единицы программного обеспечения должны быть описаны с точки зрения желаемого поведения программируемого устройства.

BDD (Behavior-Driven Development, Разработка через поведение) BDD — это методология разработки программного обеспечения, фокусирующаяся на поведении системы и тесной взаимосвязи между разработчиками, тестировщиками и бизнес-аналитиками.

Behavior-driven_development

cucumber-rust

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

В то время как в экосистеме Rust есть несколько ящиков со стилем тестирования BDD (наиболее зрелым из них является ящик cucumber_rust), использовать их в соответствии со стилем BDD необязательно (поскольку они могут быть слишком сложными для некоторых тривиальных случаев). Ничто не мешает вам следовать стилю BDD в обычных тестах Rust.

❌ Итак, вместо:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_hash() {
        let h = hash("some_string");
        
        assert_eq!(h.len(), 64);
        assert!(!h.contains("z"));
    }
}

✅ Вы всегда можете написать это более осмысленно:

#[cfg(test)]
mod hash_spec {
    use super::*;
    
    #[test]
    fn has_64_symbols_len() {
        assert_eq!(hash("some_string").len(), 64);
    }
    
    #[test]
    fn contains_hex_chars_only() {
        assert!(!hash("some_string").contains("z"));
    }
}

Это делает тесты более детализированными (а значит, более значимыми ошибками тестирования), а намерения тестирования становятся более понятными для читателя.

TDD

(Test-Driven Development, Разработка через тестирование)

TDD — это методология разработки программного обеспечения, в которой написание тестов предшествует написанию кода. Основная идея заключается в том, чтобы писать небольшие тесты для новых функций или исправлений, затем писать минимальный код для прохождения этих тестов и, наконец, рефакторить код, сохраняя при этом тесты зелеными.

testing

  • верхний e2e - проверка работы программы через внешний интерфейс
  • средний Интеграционный - проверка работы класса тоже внешний как e2e
  • нижний Модульный - проверка работы метода ( mock-и и stub-ы, unit)

Unit проверяют исключительно функционал конкретного модуля (чаще всего, класса), т.е. тот, что зависит только от самого модуля и ни от чего чего более.

Интеграционные проверяют корректность работы отдельных "модулей", если их собрать вместе согласно архитектурe. Например, работает ли правильно "Корзина", состоящая, в свою очередь, из 10 классов (предварительно проверенных модульными тестами), или "Корзина", подключенная к "Вебморде" и т.д. Где-то повыше в этой иерархии есть такие интеграционные тесты, которые проверяют конкретный функционал всей системы. Например, отправляется ли юзеру мейлом копия оплаченного заказа...

e2e (end to end) это такие интеграционные тесты, которые воздействуют на систему через ее самые внешние интерфейсы и проверяют ожидаемую реакцию системы через эти же интерфейсы. Вы загружаете все приложение целиком и имитируете действия пользователей. Причем взаимодействие происходит насквозь. От кнопочки в браузере до запросов в базе данных на сервере (если есть бэкэнд). Это самый медленный вид тестирования и им стоит покрывать только позитивные сценарии.

Логирование в тестах

is_test(true) предотвращает конфликты инициализации в тестах.

#[test]
fn test_execute_command() {
    let _ = env_logger::builder().is_test(true).try_init();
    
    info!("Starting test_execute_command");
    let result = execute_command("echo test");
    assert!(result.is_ok(), "Command failed: {}", result.unwrap_err());
}

Инструменты покрытия кода тестами

crate cargo-tarpaulin

Он покажет вам статистику покрытия, гарантируя , что вы всегда будете в курсе неизведанных территорий вашей кодовой базы .

$ cargo install cargo-tarpaulin
$ cargo tarpaulin

$ cargo tarpaulin --ignore-tests
# вычислит покрытие кода для вашего приложения, исключая при этом тестовые функции.

Генерация тестов макросами


macro_rules! test_words {
    (
        $(  // begin a repetition ($)
            // 
            // our tests will use this format:
            // 
            //    test_name : input -> expected_output
            // 
            $test_name:ident : $in:literal -> $expected:expr
        )+  // end repetition: at least 1 test is required (+)
    ) => {
        $(  // begin repetition. All code in this block will repeat
            // for every complete match found by the matcher (above).

            #[test]
            fn $test_name() {
                // run the `words` function with the provided input ($in)
                let actual = words($in);
                // make the assertion
                assert_eq!($expected, actual);
            }

        )+  // end repetition
    };
}
fn main(){
 test_words![
    ignores_period: "Hello friend."   -> vec!["Hello", "friend"]
    ignores_comma: "Goodbye, friend." -> vec!["Goodbye", "friend"]
    ignores_semicolon: "end; sort of" -> vec!["end", "sort", "of"]
    ignores_question_mark: "why?"     -> vec!["why"]
    separates_dashes: "extra-fun"     -> vec!["extra", "fun"]

    separates_by_comma_without_space:
      "Goodbye,friend." -> vec!["Goodbye", "friend"]

    apostrophe_is_one_word:
      "let's write macros" -> vec!["let's", "write", "macros"]
 ];
}

crate assert_cmd - призван упростить процесс интеграционного тестирования интерфейсов командной строки

#[test]
fn cargo_compile_simple() -> Result<(), Box<dyn std::error::Error>>{
    use assert_fs::prelude::*;
    use assert_cmd::prelude::*;
    use predicates::prelude::*;
    use assert_cmd::cmd::Command;

    let mut binding = Command::cargo_bin("example_test").expect("bin file not found");
    let mut cmd = binding.timeout(std::time::Duration::from_secs(1));

    cmd.arg("hello");
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("args:hello"))
        .code(0);
    Ok(()) 
}

wrk — это утилита командной строки для тестирования производительности HTTP-серверов.

wrk позволяет имитировать интенсивную нагрузку, отправляя множество одновременных запросов с нескольких потоков и соединений. Это делает ее идеальной для тестирования производительности REST API, веб-сайтов и других HTTP-сервисов.

wrk -c <concurrency> -d 30s "http://<host>:<port>/tasks"

wrk -t4 -c100 -d30s -s request.lua http://example.com

wrk выведет подробный отчёт, который будет включать:

  • Latency Distribution: Распределение задержек (response time) в процентилях (например, 50%, 90%, 99%).

  • Requests/sec: Среднее количество запросов в секунду.

  • Transfer/sec: Скорость передачи данных в секунду.

  • Request counts: Общее количество выполненных запросов, а также количество ошибок.

crate cargo-nextest - полная замена cargo test, предлагает более чистый интерфейс результатов теста, а также работает быстрее

$ cargo install cargo-nextest --locked
$ cargo nextest run

Особенности:

  • Параллельное выполнение тестов с высокой производительностью.
  • Поддержка изоляции тестов (sandboxing), что предотвращает конфликты при доступе к ресурсам.
  • Улучшенные отчёты о тестах, включая поддержку форматов для CI/CD систем.
  • Настраиваемое поведение через конфигурационный файл.

CLI-тестирование

Прочие тесты

Вот какие еще автоматизированные тесты существуют:

  • Тестирование доступности. Особенно актуально для мобильных, веб и деск­топных приложений. Автоматизировать такие тесты довольно сложно.
  • Тестирование безопасности. Некоторые из них можно автоматизировать, тогда как другие требуют ручного выполнения.
  • Тестирование совместимости. Проверка того, что ПО работает как ожидается на различных устройствах или операционных системах.
  • Тестирование производительности. Такие тесты измеряют задержку или отзывчивость системы.
  • Исследовательское тестирование. Оно включает в себя симуляцию использования продукта пользователями для выявления граничных случаев.

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

  • Модульное/Unit тестирование. (прям в файле, приватные методы тестирует, компилируется в исполняемый файл)
  • Doc - тестирование. (в документации к примерам использования)
  • Интеграционное тестирование. (в отдельной папке tests, тестирует публичные методы)

Если тест написан в модуле tests в виде модуля то он не попадет в бинарник

mod user{
 ...
 #[cfg(test)]
 mod test{
    #[test]
    fn success(){
        assert_eq!(1,1);
    }
   }
}

Запуск:

cargo test --  --nocapture

Проблемы для тестирования:

  • Часто имеет дело с пользовательским вводом, читает файлы и записывает выходные данные.
  • Нет доступа к коду для его тестирования (к примеру код спаггети где весь функционал вместе). p.s. надо сразу учитывать как протестировать этот функционал
  • После изменения API вам придется переписывать тестируемый код, возможно инкапсуляция интерфейс API check функцией вы избавитесь от переписывания всех тестов этого API (Решение состоит в том, чтобы написать тесты для функций таким образом, чтобы они были независимы от кода.)
  • Многослойные архитектуры должны включать интегрированные тесты для каждого уровня L1 <- Tests L1 <- L2 <- Tests

Негласные правила:

  • имена тестов должны ясно показывать что тестировали (BDD)
  • проверять одну функциональность за раз
  • не тестируйте то что знаете, тестируйте функциональность отбросив контекст своего знания о библиотеке (как "черный ящик")
  • Тестируйте функциональность, а не реализацию (Это похоже на просмотр футбольного матча: вас больше волнует гол, чем каждый пас, ведущий к нему)
  • В общем, вы всегда должны стремиться тестировать поведение, а не функции, классы или модули. Это помогает исключить детали реализации из вашего теста и уменьшает зависимости.

Рекомендация по архитектуре тестов

matklad/Delete-Cargo-Integration-Tests

Рекомендация крупным проектам иметь только один крейт(папку) для интеграционных тестов с несколькими модулями(файлами)

# ❌ То есть не делайте этого:  
tests/
  foo.rs
  bar.rs
 
# ✅ Вместо этого сделайте это:  
tests/
  integration/
    main.rs
    foo.rs
    bar.rs

Рекомендация по архитектуре тестов

Для библиотеки с общедоступным API, опубликованным на crates.io, я избегаю модульных тестов. Вместо этого я использую один интеграционный тест, называемый ( integration test ):it

✅ Для не больших crates:   
tests/
  it.rs

✅ Или для больших crates:  
tests/
  it/
    main.rs
    foo.rs
    bar.rs

Интеграционные тесты используют библиотеку как внешний контейнер. Это вынуждает использовать тот же общедоступный API, который используют потребители, что приводит к улучшению обратной связи при проектировании.

Рекомендация по архитектуре тестов

Что касается внутренней библиотеки, я вообще избегаю интеграционных тестов.

Вместо этого я использую модульные тесты

src/
  lib.rs
  tests.rs
  tests/
     mod.rs
     integration_tests/
        foo.rs
        mod.rs
        bar.rs
     
# где: 
lib.rs
  #[cfg(test)]
  mod tests;

mod.rs:
  #[cfg(test)]
  mod integration_tests;

integration_tests/mod.rs:
  #[cfg(test)]
  mod foo;
  #[cfg(test)]
  mod bar;

Запуск:

$ cargo run tests integration_tests

Ассорти трюков

Тесты документации выполняются крайне медленно.

Cargo.toml:

[lib]
doctest = false

# Даже если вы придерживаетесь модульных тестов, библиотека перекомпилируется дважды: один раз с  --test, и один раз без --test

[lib]
test = false

Отличие для библиотеки и бинарного файла

  • якщо ти пишеш лібу, бо нею будуть користуватися програмісти то тести це одне із найкращого, що може бути.

  • якщо ти пишеш бінарник, то часто простими /examples ти не обійдешся, бо треба показати як твоїм бінарником користуватися, як запускати, що очікувати, тут скоріше дока та мануал краще підходять

Тестовые функции должны быть свободными, мономорфными функциями, не принимающими аргументов, а возвращаемый тип должен реализовывать трейт std::process::Termination

  • macro assert!
  • macro assert_eq!
  • macro assert_ne!
  • macro debug_assert!

crate spectral - различные assert

use std::process::Termination;
/*
Реализации трейта Termination:
- ()
- std::result::Result<T, E> where T: Termination, E: Debug
- ! - (представляет собой тип вычислений, которые никогда не приводят к какому-либо значению.)
- std::convert::Infallible - (Тип ошибки для ошибок, которые никогда не могут произойти.)
- std::process::ExitCode
 
 классифицирует тест как пройденный или не пройденный в зависимости от того, ExitCode соответствует ли результат успешному завершению.
*/

// cargo test -p adder
#[cfg(test)]
 mod test{
    #[test]
    fn success1()->(){
        // return ()
        // default
        assert_eq!(1,1);
    }
    #[test]
    fn success2() -> std::result::Result<(),std::io::Error>{
        // return Result
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(std::io::Error::new(std::io::ErrorKind::Other, "two plus two does not equal four"))
        }
    }
    #[test]
    fn success3() -> std::process::ExitCode{
        // return ExitCode 
        assert_eq!(1,1);
        std::process::ExitCode::SUCCESS
    }
    #[test]
    fn success4(){
        // return ! 
        assert_eq!(1,1);
        std::process::exit(0x0100)
    }
}

// Оптимизированная сборка не будет выполнять debug_assert! операторы, если они `-C debug-assertions` не переданы компилятору. 
// Это debug_assert! полезно для проверок, которые слишком дороги для присутствия в сборке выпуска, но могут быть полезны во время разработки.

fn blabla(){
   let x = true;
   debug_assert!(x, "x wasn't true!");
}

Doc - тестирование

(прям в документации к примерам)

Файл ex/src/lib.rs:

/// First line is a short summary describing function.
///
/// The next lines present detailed documentation. Code blocks start with
/// triple backquotes and have implicit `fn main()` inside
/// and `extern crate <cratename>`. Assume we're testing `ex` crate:
///
/// &#96;&#96;&#96;rust
/// extern crate ex;
/// use ex::{add};
///
/// let result = ex::add(2, 3);
/// assert_eq!(result, 5);
/// &#96;&#96;&#96;
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Запуск:

$ cargo test --doc

При ошибке исполнения: Couldn't run the test: Permission denied (os error 13) - maybe your tempdir is mounted with noexec? Нужно перемонтировать папку.

$ sudo mount -o remount,exec /tmp

Тесты в документации уровня модуля //! (код в документации проходит тесты) работает только с библиотеками

в модуле  /phrases/src/english/mod.rs
//! Контейнер `adder` предоставляет функции сложения чисел.
//!
//! # Examples
//!
//! &#96;&#96;&#96;rust
//!  assert_eq!("Hello!".to_string(),  phrases::english::greetings::hello())
//! &#96;&#96;&#96;
pub mod greetings;

pub mod farewells;

Тесты в документации уровня функции /// (код в документации проходит тесты) работает только с библиотеками

В файле модуля  phrases/src/english/greetings.rs
/// Эта функция прибавляет 2 к своему аргументу.
///
/// # Examples
///
/// &#96;&#96;&#96;rust
/// use phrases::english::greetings::hello;
///
/// assert_eq!("Hello1".to_string(),  hello());
/// &#96;&#96;&#96;
pub fn hello() -> String {
    "Hello!".to_string()
}

Игнорировать выполнение тестов в документации, добавить слово "text" к блоку кода

/// &#96;&#96;&#96;text
/// fn foo() {
///    ...
/// }
/// &#96;&#96;&#96;

Как тестировать stdout вывод

tutorial/testing

// Этот вариант не сможем протестировать, нет доступа к выводу
fn find_matches(content: &str, pattern: &str) {
    for line in content.lines() {
        if line.contains(pattern) { println!("{}", line); }
    }
}
// Этот вариант мы можем протестировать
fn find_matches(content: &str, pattern: &str, mut writer: impl std::io::Write) {
    for line in content.lines() {
        if line.contains(pattern) { writeln!(writer, "{}", line); }
    }
}
#[test]
fn find_a_match() {
    let mut result = Vec::new();
    find_matches("lorem ipsum\ndolor sit amet", "lorem", &mut result);
    assert_eq!(result, b"lorem ipsum\n");
}
fn main() -> Result<()> {
    let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path).with_context(|| format!("could not read file `{}`", args.path.display()))?;
    find_matches(&content, &args.pattern, &mut std::io::stdout());
    Ok(())
}

use std::io::{BufWriter, Write};
#[test]
fn test_stdout() {
    let stdout = std::io::stdout();
    let mut writer = BufWriter::new(stdout.lock());
    your_function(&mut writer);
    let output = writer.buffer();
    assert_eq!(output, b"Expected output");
}

Как тестировать stdout вывод

Первый вариант тестирования stdout/stderr через передачу писателя в аргументах функции.

Недостаток, то что по всей цепочке вызовов передается &Mutex<dyn Write + Send>

use std::rc::Rc;
use std::fmt::Write as _;
use std::io::{self, Write};

pub struct Service<T: AIClient> {
    client: T,
}
fn print_stderr_result(
        &self,
        output: &[u8],
        stderr: &Mutex<dyn Write + Send>,
    ) -> Result<(), Error> {
        if !output.is_empty() {
            let mut handler = stderr.lock().unwrap(); 
            handler.write_all(output).map_err(Error::Io)?;
            handler.write_all(b"\n").map_err(Error::Io)?;
            handler.flush().map_err(Error::Io)?;
        }
        Ok(())
}
#[test]
fn test(){
 service.print_stderr_result(err.to_string().as_bytes(), &Mutex::new(io::stderr()));

 let mut result_stderr = Mutex::new(Vec::new());
 service.print_stderr_result(err.to_string().as_bytes(), &mut result_stderr);
 let guard = result_stderr.lock().unwrap();
 let output_stderr = String::from_utf8_lossy(&guard);
 assert!(output_stderr.len() > 0);
}

Второй вариант держать свойство с писателем и использовать его только в нужном методе.

Недостаток, лишнее поле только для нужд тестирования.

pub struct Service<T: AIClient> {
    client: T,
    pub writer: Option<StdoutWriter>,
}
type StdoutWriter = Rc<Mutex<dyn Write + Send>>;
 
fn print_stderr_result(
    &self,
    output: &[u8],
) -> Result<(), Error> {
    if !output.is_empty() {
        if let Some(w) = &self.writer{
            let mut handler = w.lock().unwrap();
            handler.write_all(output).map_err(Error::Io)?;
            handler.write_all(b"\n").map_err(Error::Io)?;
            handler.flush().map_err(Error::Io)?;
        }else{
                let stderr = io::stderr();
                let mut handler = stderr.lock();
                handler.write_all(output).map_err(Error::Io)?;
                handler.write_all(b"\n").map_err(Error::Io)?;
                handler.flush().map_err(Error::Io)?;
        }
    }
    Ok(())
}
#[test]
fn test(){
    let mut test_stderr = Rc::new(Mutex::new(Vec::new()));
    service.writer = Some(test_stderr.clone());
    service.print_stderr_result(err.to_string().as_bytes());
    let guard = test_stderr.lock().unwrap();
    let output_stderr = String::from_utf8_lossy(&guard);
    assert!(output_stderr.len() > 0);
}

Как тестировать stdout вывод

Прокинуть через TestImplWriter буффер Rc<RefCell<Vec<u8>>>, что дает возможно получить данные уже после move TestImplWriter в Service


pub trait Writer{ fn print_stdout_result(&self, output: &[u8]) -> Result<(), Error>;}
pub struct Service {  writer: W}
impl Service {
    pub fn new() -> Self { Self { writer: ImplWriter::default() } }
}
impl Service {
    pub fn new_with_writer( writer: W ) -> Self { Self { writer}}
   // ... весь функционал тут
}
// Default для вызова new без передачи Writer по умолчанию
#[derive(Default)]
pub struct ImplWriter;
impl Writer for ImplWriter {
    fn print_stdout_result(&self, output: &[u8]) -> Result<(), Error> { // Sends [u8] data to the io::stdout output stream
        if !output.is_empty() { 
            let stdout = io::stdout();
            let mut handler = stdout.lock();
            handler.write_all(output).map_err(Error::Io)?;
            handler.flush().map_err(Error::Io)?;
        } Ok(()) }
}
impl Service {
    pub fn new() -> Self { Self { writer: W::default() } }
    pub fn new_with_writer( writer: W) -> Self { Self { writer} }  // можно использовать при тестировании
    pub fn example(&self) {
        self.writer.print_stdout_result("123".as_bytes());      
    }
}
fn main(){  let service = Service::new(); /* default отработает*/ }
Как тестировать
mod test{
  // Своя реализация Writer
  pub struct TestImplWriter{
    pub stdout: Rc>>, 
  }
  impl Default for TestImplWriter {
    fn default() -> Self { Self { stdout: Rc::new(RefCell::new(Vec::new())) }}// не будет использоваться
  }
  impl  TestImplWriter {
    pub fn new(&mut self,stdout: Rc>>){ self.stdout = stdout; }
  }
  impl Writer for TestImplWriter {
    fn print_stdout_result(&self, output: &[u8]) -> Result<(), errors::Error> {
        if !output.is_empty() {  self.stdout.borrow_mut().extend_from_slice(output); } Ok(())
    }
}
#[test]
fn test(){
     let result_stdout = Rc::new(RefCell::new(Vec::new()));
     let mut writer = TestImplWriter::default();
     writer.new(result_stdout.clone(), result_stderr.clone());

     let service = Service::new_with_writer( writer );
     service.example();
     let guard = result_stdout.borrow();
     let output_str = String::from_utf8_lossy(&guard);
     assert_eq!(output_str, "123");
}}

Как тестировать stdout вывод

Вы можете использовать макросы для захвата stdout и проверки его содержимого. Популярный вариант - использовать макросы assert_*! из библиотеки assert_cmd.

use assert_cmd::Command;

#[test]
fn test_stdout() {
    let mut cmd = Command::cargo_bin("your_binary").unwrap();
    cmd.assert().success().stdout("Expected output\n");
}

Как тестировать ожидая аргументы командной строки

[dev-dependencies]
assert_cmd = "2.0.11"
predicates = "3.0.3"
use assert_cmd::prelude::*; // Add methods on commands
use predicates::prelude::*; // Used for writing assertions
use std::process::Command; // Run programs

#[test]
fn file_doesnt_exist() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::cargo_bin("YOUR CRATE NAME LIBRARY")?;

    cmd.arg("foobar").arg("test/file/doesnt/exist");
    cmd.assert()
        .failure()
        .stderr(predicate::str::contains("could not read file"));

    Ok(())
}

Как тестировать работу с файлами

[dev-dependencies]
assert_fs = "1.0.13"

use assert_fs::prelude::*;

#[test]
fn find_content_in_file() -> Result<(), Box<dyn std::error::Error>> {
    let file = assert_fs::NamedTempFile::new("sample.txt")?;
    file.write_str("A test\nActual content\nMore content\nAnother test")?;

    let mut cmd = Command::cargo_bin("grrs")?;
    cmd.arg("test").arg(file.path());
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("test\nAnother test"));

    Ok(())
}

Как успеть очистить env среду разработки после panic в тесте

#[tokio::test]
async fn setup_create_account() {
    // setup env ...
  
    // run test
    let join_handle = tokio::spawn(async {
        test_unified().await?;
        Ok::<(), io::Error>(())
    });

    let err = match join_handle.await {
        Ok(Err(e)) => Some(Box::new(e)),
        Err(e) => Some(Box::new(e.into())),
        Ok(Ok(_)) => None,
    };

    // clear env ...

    if let Some(err) = err {
        println!("Test failed, rethrowing panic...");
        panic::resume_unwind(err);
    }
}

Как тестировать асинхронность

crate ditto_time - тестирование асинхронной среды

crate tokio-test

async-tests-tokio-rust

для обычных юнит-тестов асинхронного кода есть крайне удобный макрос #[tokio::test]. В tokio, в том числе, можно вручную управлять временем, а в crate tokio-test есть примитивы для мока I/O, например.

для более глубокой проверки можно воспользоваться инструментом loom


Test async function

[dependencies]
actix-rt = "*"
#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
  use super::*;
  
  #[test]
  fn test_str_len() {
    assert_eq!(str_len("x5ff"), 4);
  }

  #[actix_rt::test]
  async fn test_str_len_async() {
    assert_eq!(str_len_async("x5ff").await, 4);
  }
}

[dev-dependencies]
tokio-test = "*"
#[tokio::test]
async fn test_str_len() {
    assert_eq!(str_len("x5ff"), 4);
}

Модульное/Unit тестирование.

В Unit тесте можно протестировать приватные функции (они в одной области видимости)

так как модульные тесты идут в тех же файлах что и основной код, вы будете использовать #[cfg(test)] чтобы указать, что они не должны быть включены в скомпилированный результат.

how-do-i-test-private-methods-in-rust

pub use foo::*;
pub mod foo{
    fn ggg()->bool{
        true
    }
    pub fn ddd()->bool{
        true
    }

    #[cfg(test)]
    mod event_message_spec {
    use super::*;
         #[test]
        fn test_private() {
            print!("test");
            assert!(ggg())
        }
    }
}
#[cfg(test)]
mod event_message_spec {
use super::*;
     #[test]
    fn test_public() {
        print!("test");
        assert!(ddd())
    }
}

Запуск тестов из папки examples

Каждый файл Rust в examples/(или каждый подкаталог в examples/, который включает main.rs) может быть запущен как автономный двоичный файл с

$ cargo run --example <name> 
$ cargo run --example <name> -- arg1 arg2
или 
$ cargo test --example <name>

Эти программы имеют доступ только к публичному API вашего ящика и предназначены для иллюстрации использования вашего API в целом. Примеры не обозначены специально как тестовый код (нет #[test], нет #[cfg(test)]), и они являются плохим местом для размещения кода, который проверяет скрытые уголки и щели вашего ящика, особенно потому, что примеры не запускаются cargo test по умолчанию.

Запуск тестов из папки examples. (При запуске $ cargo test папка /examples не просматривается)

cargo test --examples

Для запуска тестов из папки examples при вызове $ cargo test следует расшарить папку examples в папку интеграционных тестов

#[path = "../examples"]
mod examples {
   mod check_lib;
}

Так же можно задать пути к тестам через Cargo.toml:

[[example]]
name = "my_test"
path = "examples/file.rs"
required-features = ["some_feature"]  # если нужны определенные фичи

Запуск:

cargo run --example my_test --features "some_feature"

Интеграционное тестирование

Интеграционные (Integration) тесты (в директории tests/)

Интеграционное тестирование. (lib должен быть) (в отдельной папке tests, тестирует публичные методы)

Каждый файл в папке tests/ скомпилирован как отдельный ящик.

Интеграционные тесты являются внешними по отношению к вашему ящику и используют только его открытый интерфейс так же, как любой другой код.

Запуск определенной функции из всех файлов интеграционных тестов

cargo test --test integration_test

Если наш проект является бинарным крейтом, который содержит только src/main.rs и не содержит src/lib.rs, то в таком случае, мы не можем создать интеграционные тесты в папке tests и подключить функции определённые в файле src/main.rs в область видимости с помощью выражения use. Только библиотечные крейты могут предоставлять функции, которые можно использовать в других крейтах; бинарные крейты предназначены только для самостоятельного запуска.

Это одна из причин того, что Rust проекты для выполняемой программы имеют просто файл src/main.rs, который вызывает логику, которая находится в файле src/lib.rs. Используя такую структуру, интеграционные тесты могут протестировать библиотечный крейт с помощью use, чтобы подключить важную функциональность и сделать её доступной.

Каждый файл интеграционного теста компилируется как отдельный контейнер, что может отрицательно повлиять на время компиляции тестов. Группировка похожих тестов в одном файле может помочь уменьшить это влияние.


В Rust тесты интеграции полностью нарушают вашу библиотеку. Они используют вашу библиотеку так же, как и любой другой код, что означает, что они могут вызывать только функции, которые являются частью общедоступного API вашей библиотеки. Их цель - проверить, правильно ли работают многие части вашей библиотеки. Единицы кода, которые работают правильно сами по себе, могут иметь проблемы при интеграции, поэтому также важна проверка охвата интегрированного кода.

Интеграционное тестирование

Файл ex/src/lib.rs:

use std::result::Result;  
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
fn sqrt(number: f64) -> Result<f64, String> { <<<--- эту приватную функция можно тестировать только в самом модуле
    if number >= 0.0 {
        Ok(number.powf(0.5))
    } else {
        Err("negative floats don't have square roots".to_owned())
    }
}

Файл ex/Cargo.toml:

[package]
name = "ex"

[[test]]
name = "integration"
path = "tests/lib.rs"

Для тестирования в папке tests должен быть любой файл с тестами

Файл ex/tests/lib.rs:

extern crate ex;
mod function; // подключим еще одну папку с тестами
#[cfg(test)]
mod test{
    use ex::{add};
    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }
}

Файл ex/tests/function/mod.rs: (файл роутер подключает все файлы в папке)

  mod integration_test;

Файл ex/tests/function/integration_test.rs:

use ex;// `extern crate ex;` уже был во входном файле
#[test]
fn test_add() {
    // using common code.
    assert_eq!(ex::add(3, 2), 6);
}

Запуск

$ cargo test --test integration

Подмодули в интеграционных тестах

organization/test

При организации нескольких файлов в папке tests есть возможность создать общий файл с функциями который не должен компилироваться как отдельный крейт теста. Для этой цели используют папку и файл mod.rs

Файл: tests/common/mod.rs:

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Файл: tests/integration_test.rs:

use adder;
mod common;
#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

Писать тесты в модуле что б они не попадали в компилируемый исходник, в модуле разделять тесты по ф-циям что даст параллельное исполнение

Все тесты в одной функции это - плохой подход к написанию тестов. Аналогия антипаттерн GodObject из ООП. У Вас одна огромная функция test() в которой все тест-кейсы написаны просто подряд.

Это плохо, так как:

  • Падающий тест-кейс плохо локализуется.
  • При прогоне тестов не понятно сколько у Вас там assert'ов сделано.
  • Тесты хуже визуально разделены и хуже читаются.
  • Не понятно, а что вообще тестируется и в чём смысл тестов.
  • Увеличивается время прогона тестов, ибо они вынуждены выполняться последовательно.

Правильный подход - выносить каждый тест-кейс в отдельную тест-функцию. Имена тест-функциям давать не "от балды", а таким образом, что-бы они раскрывали смысл теста и оставались лаконичными.

Пример:

#[test]
fn none_on_unknown_date() {
    assert!(User::new(2017, 2, 29).is_none());
}

Result вместо panic!

something()?.something()?

writing/test

Возвращаем Ok(()) когда тест успешно выполнен и Err со String внутри, когда тест не проходит.

Позволяет использовать оператор ? "вопросительный знак" в теле тестов, который может быть удобным способом писать тесты, которые должны выполниться не успешно, если какая-либо операция внутри них возвращает вариант ошибки Err. Нельзя использовать аннотацию #[should_panic]

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

Тесты в модуле test тестирование небольшого кусочка функциональности), полностью исключены из обычной сборки благодаря аттрибуту

#[cfg(test)]

Любой код, помеченный #[test] или внутри #[cfg(test)] модуля, полностью исключается из финального бинарника

Атрибут #[cfg(test)]

// Код компилируется ТОЛЬКО для тестов
#[cfg(test)]
mod tests {
    // Весь этот модуль исключается из финальной сборки
}

// Эквивалентно:
if cfg!(test) {
    // тестовый код
} else {
    // ничего
}

pub fn add_two(a: i32) -> i32 {
    a + 2
}
// группируем тесты вместе
// Атрибут cfg указывает на то, что тест будет скомпилирован, только когда мы попытаемся cargo test запустить тесты. 
// Но не при build построении библиотеки 
#[cfg(test)]
mod test {
    // use super::add_two;
    use super::*;// глобальное подключение

    #[test]
    fn it_works() {
        assert_eq!(4, add_two(2));
    }
}
cargo test --help 
или 
cargo test -- --help

Некоторые параметры командной строки идут к cargo test, а некоторые переходят к результирующему двоичному файлу теста.

Чтобы разделить эти два типа аргументов, вы перечисляете аргументы, за cargo test которыми следует разделитель, -- а затем те, которые идут в тестовый двоичный файл.

Запуск cargo test --help отображает параметры, которые вы можете использовать cargo test, а запуск cargo test -- --help показывает параметры, которые можно использовать после разделителя --

cargo test -- --nocapture -Z unstable-options --format=json
cargo test --lib 
cargo test --all
cargo test --doc
  • Запуск только тестов в библиотеке: cargo test --lib При запуске cargo test или cargo test --all запускаются все тесты и из папки интеграционных tests
  • Запуск только doc-тестов: cargo test --doc
  • Сборка тестов tests/без их запуска cargo test --no-run --test NAME, но вам нужно перечислить их самостоятельно. Снова, вероятно, имеет смысл добавить что-то в Cargo здесь.

Между тем интеграционные тесты - это действительно отдельные ящики, которые используют вашу библиотеку в качестве зависимости. Вы можете сделать их явными с Cargo.toml файлами,

[dependencies] 
foo = {path = "…"}

чтобы cargo test без аргументов в вашем основном ящике их не запускать.

ENV_VAR_TWO=89  cargo test --bin=environment_variables

Запуск теста в определенном файле если есть папка bin

cargo test -- timeout=5

cargo/reference/source-replacement

cargo test  --manifest-path pallets/wl/Cargo.toml

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

cargo test --  --nocapture 

cargo test -- --show-output

nocapture - печатать сообщения при тестировании

show-output - чтобы всегда видеть вывод на консоль корректно работающих программ

#[test]
fn it_works() {
    println!("Hello");
    assert!(true);
}
cargo +nightly test --lib weak -- --nocapture
cargo +nightly test weak::tests::test_sea -- --nocapture

Запуск тестов из файлов библиотеки

/home/jeka/.cargo/bin/cargo test --color=always --no-run --package social_backend --lib api::graphql::server::spec::juniper_test -- --exact

конкретный тест

File src/service/user/domain/event/mod.rs:

#[cfg(test)]
mod event_message_spec {
     #[test]
    fn deserializes() {
        print!("test");
        assert!(true)
    }
}

Запуск:

$  cargo test service::user::domain::event::event_message_spec::deserializes --  --nocapture

Фильтр тестов по имени в названии теста

cargo test my_test --bin=1  
# отработают только my_test_1 и my_test_2
#[test]
fn my_test_1() { assert!(true);}
#[test]
fn my_test_2() { assert!(true);}
#[test]
fn test_false() { assert!(false);}

Тестирует конкретные тесты

cargo test name_test

cargo test -- --test modname::test_name

Тест конкретного модуля с выводом первой ошибки

cargo test  --manifest-path pallets/palletmy/Cargo.toml --color always  --  --nocapture   2>&1 | grep error -A 20| head -40

cargo test --package minterest-protocol --lib -- tests::deposit_underlying_should_work --exact --nocapture

#[should_panic(expected = "assertion failed")] уточнение при каком тексте ошибки не давать срабатывать ошибке

#[test]
#[should_panic] если не будет panic тест сработает
fn it_works() {
    assert!(false);
}

#[test]
#[should_panic(expected = "empty input")]
fn empty_input() {
    parse("").unwrap();
}

#[should_panic] Тест считается пройденным, если код внутри функции вызывает панику

#[test]
#[should_panic]
fn it_works() {
    assert!(false);
}

#[ignore] отключение текста, например если он тяжелый

И наоборот запуск только тяжелых ignore тестов

cargo test -- --ignored
#[test]
#[ignore]
fn it_works() {
    assert_eq!(4, add_two(2));
}

no_run указывает, что код должен компилироваться, но запускать его на выполнение не требуется

cargo test --no-run
/// &#96;&#96;&#96;rust,no_run
/// loop {
///     println!("Привет, мир");
/// }
///&#96;&#96;&#96;

--features

Запуск тестов из папки tests интеграционные и компилирует код только помеченный

#[cfg(feature = "test")]

#[cfg(feature = "test")]
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Debug)]
#[cfg_attr(feature = "test", derive(PartialEq, Serialize))]
pub struct UserEmail(String);

#[cfg(feature = "test")]
impl<'de> Deserialize<'de> for UserEmail {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let email = String::deserialize(deserializer)?;

        UserEmail::new(email).ok_or_else(|| {
            serde::de::Error::custom("`email` is not valid".to_string())
        })
    }
}

Запуск только помеченных тестов #[cfg(feature = "test")]

cargo test  --features "test"

Display тестируем реализацию

#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, FromStr, PartialEq, Serialize,)]
pub struct UserId(uuid::Uuid);
use derive_more::Display;
#[cfg(test)]
mod user_id_spec {
    use super::*;
    #[test]
    fn display_uuid() {
        let value = "75fc650a-1a60-4727-bcef-939fc9b9ed39";
        let user_id = value
            .parse::<UserId>()
            .expect("Failed to parse from string");
        assert_eq!(
            value.to_owned(),
            format!("{}", user_id),
            "Display is not correct"
        );
    }

    #[test]
    fn display_default() {
        assert_eq!(
            "00000000-0000-0000-0000-000000000000".to_owned(),
            format!("{}", UserId::default()),
            "Display is not correct"
        );
    }
}

FromStr тестируем реализацию

#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, FromStr, PartialEq, Serialize,)]
pub struct UserId(uuid::Uuid);

#[test]
fn from_str() {
    let five_uuid = "05000000-0000-0000-0000-000000000000";
    let user_id = five_uuid
        .parse::<UserId>()
        .expect("Failed to parse from string");
    assert_eq!(UserId::from(5_u128), user_id, "FromStr is not correct");
}
#[test]
fn from_str_v4() {
    let value = uuid::Uuid::new_v4();
    let user_id = value
        .to_string()
        .parse::<UserId>()
        .expect("Failed to parse from string");
    assert_eq!(UserId::from(value), user_id, "FromStr is not correct");
}

Deserialize тестируем реализацию

#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, FromStr, PartialEq, Serialize,)]
pub struct UserId(uuid::Uuid);

#[test]
fn deserialization() {
    let user_id: UserId =
        serde_json::from_str("\"05000000-0000-0000-0000-000000000000\"").expect("Deserialization error");
    assert_eq!(
        UserId::from(5_u128),
        user_id,
        "Deserialization in 5 is not correct"
    );
    assert_ne!(
        uuid::Uuid::from(6_u128),
        uuid::Uuid::from(user_id),
        "Deserialization is not correct"
    );
}

Serialize тестируем реализацию

#[derive(Clone, Copy, Debug, Deserialize, Display, Eq, FromStr, PartialEq, Serialize,)]
pub struct UserId(uuid::Uuid);

#[test]
fn serialization() {
    let user_id_string = serde_json::to_string(&UserId::default()) .expect("Serialization error");
    assert_eq!(
        user_id_string,
        "\"00000000-0000-0000-0000-000000000000\"".to_owned(),
        "Serialization from default is not correct"
    );
    let user_id_string = serde_json::to_string(&UserId::from(5_u128)) .expect("Serialization error");
    assert_eq!(
        user_id_string,
        "\"05000000-0000-0000-0000-000000000000\"".to_owned(),
        "Serialization from 5 is not correct"
    );
}
[dev-dependencies]
wasm-bindgen-test = "0.3.0"

File: <you crate>/tests/web.rs:

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

    #[wasm_bindgen_test]
    fn pass() {
        assert_eq!(1, 1);
    }
}

Run:

$ wasm-pack test --node

Обнаруживает тонкие, логические баги, которые тестами не поймаешь.

TLA+ = Temporal Logic of Actions + Plus

  • Temporal Logic — позволяет описывать поведение систем во времени (что должно/может происходить на каждом шаге).
  • Actions — выражают, как состояние может меняться.
  • Plus — дополнительные конструкции и синтаксис для описания систем на практике.

Это как язык для проектирования и проверки алгоритмов, до того, как вы их закодируете.

Вместо того чтобы сразу писать код, вы сначала чётко описываете, что система должна делать, и используете инструменты TLA+ для проверки, что вы не допустили логических ошибок.

TLA+ поможет проверить это до имплементации, если ты проектируешь:

  • параллельные очереди,
  • системы с потоками/акторами,
  • распределённые сервисы,
  • FSM/протоколы (например, авторизация, лидерство)