| Улучшение тестов RestAssured.Net при помощи мутаций и Stryker.NET |
| 12.01.2026 00:00 |
|
Когда я разрабатываю и выпускаю новые функции или исправления ошибок для RestAssured.Net, я сильно полагаюсь на приёмочные тесты, которые постепенно писал. Помимо того, что они служат живой документацией для библиотеки, я запускаю эти тесты как локально, так и при каждом пуше на GitHub для разных версий .NET, чтобы убедиться, что ничего по случайности не сломал. Но насколько на самом деле надёжны эти тесты? Могу ли я верить, что они будут проходить успешно и падать именно тогда, когда нужно? Покрыл ли я все важные моменты? Я регулярно говорю и пишу об этом, а также обучаю важности тестирования своих тестов, поэтому логично начать применять это на практике и получить больше понимания о качестве набора тестов RestAssured.Net. Один из подходов к изучению качества тестов — это техника, называемая мутационным тестированием. Когда я говорю о тестировании тестов, я демонстрирую это с применением мутационного тестирования (недавнюю лекцию можно посмотреть здесь), но до сих пор я в основном использовал PITest для Java. Поскольку RestAssured.Net — библиотека на C#, я не могу использовать PITest, но слышал много хорошего о Stryker.NET – это был идеальный шанс наконец испробовать его в деле. Добавление Stryker.NET в проект RestAssured.Net Первым шагом было добавление Stryker.Net в проект RestAssured.Net. Stryker.NET — это инструмент для dotnet, поэтому установка проходит просто: выполните команду dotnet new tool-manifest чтобы создать новый манифест инструментов, специфичный для проекта (это был первый локальный dotnet tool для этого проекта), а затем dotnet tool install dotnet-stryker чтобы добавить Stryker.NET в проект как dotnet tool. Первый запуск мутационных тестовЗапуск мутационных тестов с помощью Stryker.NET также прост: dotnet stryker --project RestAssured.Net.csproj из папки с проектом тестов — этого достаточно.
Поскольку и мой набор тестов (около 200), и сам проект относительно небольшие по объёму кода, и тесты выполняются быстро, запуск мутационных тестов для всего проекта мне подходит. Процесс всё равно занял около пяти минут. Если у вас более крупная кодовая база и более объемные наборы тестов, вы увидите, что мутационное тестирование займёт намного, намного больше времени. В таком случае, вероятно, лучше начать с подмножества вашего кода и подмножества тестов. Через пять минут с небольшим появились результаты: Stryker.NET создал 538 мутаций из кода моего приложения.
Из них:
Это даёт общий показатель мутационного тестирования 59,97%. Хорошо это или плохо? Если честно, я не знаю, и да это и не важно. Как и с покрытием кода, я не сторонник установки фиксированных целей для такого рода метрик, так как это обычно приводит к написанию тестов ради улучшения показателя, а не ради реального улучшения кода. Гораздо больше меня интересует информация, которую Stryker.NET предоставил в процессе мутационного тестирования. Открытие HTML-отчётаМеня удивило, что Stryker.NET сразу же создаёт очень презентабельный и невероятно полезный HTML-отчёт. Он предоставляет как обзор результатов на высоком уровне:
так и подробную информацию по каждой мутации, которая была убита или выжила. Отчёт разбивает результаты по пространствам имён и классам и служит отправной точкой для более детального изучения результатов по отдельным мутациям. Давайте посмотрим, дает ли отчёт полезную, практическую информацию для улучшения набора тестов RestAssured.Net. Отсутствующее покрытиеКак и многие другие инструменты мутационного тестирования, Stryker.NET дает информацию о покрытии кода вместе с информацией о покрытии мутаций. То есть, если в коде приложения есть участки, которые были мутированы, но не покрыты ни одним тестом, Stryker.NET сообщит об этом. Вот пример:
Stryker.NET изменил сообщение исключения, которое выбрасывается, когда RestAssured.Net пытается десериализовать тело ответа, которое либо null, либо пустое. Очевидно, что в наборе тестов нет теста, покрывающего этот путь в коде. Так как этот конкретный участок кода связан с обработкой исключений, разумно добавить тест для него: [Test] Я добавил соответствующий тест в этом коммите. Удаление блоков кодаЕщё один тип мутаций, которые генерирует Stryker.NET, — это удаление блока кода. Согласно отчёту, похоже, что ряд таких мутаций не выявляется ни одним тестом. Вот пример:
Оператор return в методе Put(), используемом для выполнения HTTP PUT операции, заменён на пустое тело метода, но ни один тест это не выявляет. То же самое касается методов для HTTP PATCH, DELETE, HEAD и OPTIONS. Если посмотреть на тесты, которые покрывают разные HTTP-методы, это имеет смысл. Хотя я вызываю каждый из этих методов в тесте, я проверяю результаты для вышеупомянутых HTTP-методов. Я фактически полагаюсь на то, что при вызове Put() не будет выброшено исключение, и на этом основываю утверждение «работает». Давайте исправим это, хотя бы проверяя одно из свойств ответа, возвращаемого при использовании этих HTTP-методов: [Test] Эти проверки были добавлены в набор тестов RestAssured.Net в этом коммите. Улучшение тестируемостиСледующий сигнал, который я получил из этого первоначального запуска мутационных тестов, оказался весьма интересным. Он показывает, что даже несмотря на то, что у меня есть приёмочные тесты, которые добавляют cookies в запрос и проходят только если запрос содержит заданные cookies, я не полностью покрываю некоторую добавленную логику:
Чтобы понять, что здесь происходит, полезно знать, что в C# у Cookie есть конструктор, который создаёт Cookie, указывая только имя и значение, но при этом cookie должен иметь установленное значение домена. Чтобы это обеспечить, я добавил логику, которую вы видите на скриншоте. Однако Stryker.NET сообщает, что я не тестирую эту логику должным образом, потому что изменение её реализации не вызывает провалов тестов. Возможно, я мог бы протестировать эту конкретную логику с помощью нескольких дополнительных приёмочных тестов, но это действительно лишь небольшой кусок логики, и я должен иметь возможность протестировать его изолированно, верно? Ну, не с кодом, написанным так, как сейчас… Итак, пора вынести этот кусок логики в отдельный класс, что как улучшит модульность кода, так и позволит тестировать его изолированно. Сначала вынесем логику в класс CookieUtils: internal class CookieUtils Я сознательно сделала этот класс внутренним, чтобы он не был напрямую доступен пользователям RestAssured.Net. Однако, поскольку мне нужно обращаться к нему в тестах, я добавил этот небольшой фрагмент в файл RestAssured.Net.csproj: <ItemGroup> Теперь я могу добавить юнит-тесты, которые должны покрывать оба пути в логике SetDomainFor(): [Test] [Test] Эти изменения были добавлены в исходный код и тесты RestAssured.Net в этом коммите. Интересная мутацияДо этого момента все предупреждения, появившиеся в отчёте мутационного тестирования Stryker.NET, были ценными: они указывали на код, который ещё не покрыт тестами, на тесты, которые можно улучшить, и вели к рефакторингу кода для улучшения тестируемости. Использование Stryker.NET (и мутационного тестирования в целом) иногда приводит к довольно, кхм, любопытным мутациям – например, такой:
Я проверяю, что определённая строка либо равна null, либо пуста, и если выполняется одно из этих условий, RestAssured.Net выбрасывает исключение. Абсолютно корректно. Однако Stryker.NET меняет логическое ИЛИ на логическое И (что является распространённой мутацией), что делает выполнение условия невозможным. Стоит ли вообще делать такую мутацию? В некоторой степени — да. Даже если код после мутации перестаёт иметь смысл, это показывает, что ваши тесты для этого логического условия, вероятно, требуют доработки. В данном случае мне не нужно добавлять дополнительные тесты, так как мы уже обсуждали это выражение ранее (помните, что оно вообще не имело покрытия тестами). Тем не менее, это заставило меня ещё раз взглянуть на это выражение, и только тогда я понял, что могу упростить этот фрагмент кода до: if (string.IsNullOrEmpty(responseBodyAsString)) Вместо самодельного логического OR я теперь использую встроенную конструкцию C#, которая, безусловно, более безопасна. В общем, если ваш инструмент мутационного тестирования генерирует несколько (или даже много) мутаций для одного и того же выражения или блока кода, возможно, стоит ещё раз взглянуть на этот код и посмотреть, можно ли его упростить. Это был очень маленький пример, но я считаю, что это наблюдение и в целом справедливо. Это изменение было добавлено в исходный код и тесты RestAssured.Net в этом коммите. Повторный запуск мутационных тестов и анализ результатов Теперь, когда несколько (предполагаемых) улучшений были внесены в тесты и код, давайте снова запустим мутационные тесты, чтобы увидеть, улучшились ли наши показатели. Вкратце:
Более внимательный взгляд на HTML-отчёт, созданный после этого запуска мутационного тестирования, показывает, что все предупреждения, о которых я говорил в этой статье, успешно устранены. Это прогресс. В целом, показатель мутационного тестирования вырос с 59,97% до 61,11%. Возможно, это кажется незначительным, но это определённо шаг в правильном направлении. Самое главное для меня сейчас — мои тесты для RestAssured.Net улучшились, код стал лучше, и в процессе я многое узнал о мутационном тестировании и Stryker.NET. Буду ли я запускать мутационные тесты каждый раз при внесении изменений? Наверное, нет. Информации много, её нужно обрабатывать, а это требует времени, которое не хочется тратить на каждый билд. По этой причине я также не собираюсь включать эти тесты в пайплайн сборки и тестирования RestAssured.Net, по крайней мере в ближайшее время. Тем не менее, это был очень ценный и приятный опыт, и я обязательно продолжу улучшать тесты и код RestAssured.Net, используя предложения Stryker.NET. |