TLDR; применяй сразу тут и примеры запуска на github

Пролог

Эта статья является прообразом доклада на Dotnext 2019, где я рассказал о применении подхода мутационного тестирования в проектах на .NET Core.

Доклад вызвал неоднозначную реакцию, от "Зачем это необходимо?" до "Как я жил без этого раньше".

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

Я являюсь мейтейнером одной популярной библиотеки Flurl для работы с HttpClient в виде builder. Она позволяет легко и просто составлять запрос к API.

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

И библиотека на основе Flurl, которую я разрабатываю, Ecwid CLI - это тоже вдохновленный принципам TDD клиент к хранилищу данных для интернет магазинов, построенный на паттерне builder.

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

О чем пойдет речь?

Часть 1 - вы читаете эту часть

  • тестирование и для чего применять еще один вид
  • про принцип мутационного тестирования и анализа

Часть 2

  • виды мутаций

Часть 3

  • как применить в .NET
  • когда использовать мутационное тестирование и как подготовится

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

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

В своей практике разработчики встречаются с ручным функциональным тестированием (вы это это делаете постоянно, когда вносите исправление), с автоматизированным тестированием изолированных частей (unit тестирование), с автоматическим интеграционным тестированием или UI тестированием.

Для чего нам необходим еще один вид тестирования? Мы не тестировщики и писать тесты, как разработчики, особо не любим.

Вопрос к вам?  А кто из вас перфекционист? А кто уверен в своих тестах на все 100 процентов? Что бы прям "зуб поставить", что они словят именно то, на что рассчитаны?
А есть ли у вас покрытие тестами более 70%?
А у кого ошибки в ПО повлияют на деньги клиентов, репутацию вашей компании или, не дай бог, жизнь и здоровье людей?

Для компаний и проектов с такими высокими рисками очень важно тестирование с покрытием на всей пирамиде тестирования близким к 100%, потому что другим способом проверить соответствие требованиям не можем. Или можем?‌‌

Есть один способ - это тестирование на выбранной группе лиц, например, анонимное тестирование медикаментов на группе беременных женщин по всему миру. Кто в здравом уме согласится сделать это добровольно? А если не сказать и опросить постфактум применения?

Хотели бы вы, например, поездить на Тесле, когда Илон Маск вам выкатит апдейт, который будет чуть чуть недостетирован? Или тестирование плохо кончились?

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

Самая частая причина не написания тестов - это отсутствие времени, выделенного на это.

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

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

Про цель тестирования

Так зачем нам нужно во всех случаях тестирование как таковое?

Ради красивых цифр в code coverage? Ради бейджика, что вы умеете их разрабатывать? Для строчки в резюме “Пишу код на TDD”?

Нет

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

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

Вы можете проводить code-review кода тестов, посчитать code-coverage для понимания какие участки кода покрыты тестами, вы можете запускать много раз тесты в разных окружениях, в pipeline и без него, при каждом билде, вы можете отдать ревью тестов на аудит, но в конечном итоге вы будете ориентироваться на мнение человека, а люди ошибаются или невнимательны. И все эти мероприятия полностью не покажут качество ваших тестов.

Нет других способов автоматизированно проверить ваши тесты на профпригодность. Кроме как… сломать их!

Вопрос к вам? Кто может дать гарантию того, что вы откроете в редакторе ваш код, внесете в него ошибку и ваши тесты гарантированно упадут?

Единственная возможность проверить вашу систему тестов до продакшена или до того как руки аутсорсеров в него внесут изменения - это внести в нее ошибку! Намеренно и специально. И посмотреть, что получится. Этим и занимается мутационное тестирование.

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

Парадигмы TDD-BDD и MDD

Существуют практики по разработке и по написанию тестов, которые вам хорошо известны - это TDD и BDD, разработка через тестирование и разработка на основе поведения пользователя.

Мутационное тестирование и разработку на основе мутаций давайте обзовем MDD - Mutation Driven Development. Оно пронизывает все слои тестирования и проверяет целиком систему на устойчивость.

Внимание ! Так же MDD можно описать как Main Depression Disaster, что означает основное депрессивное расстройство личности - это именно то, что вас ждет, когда вы увидите, что те тесты, которые вы долго и мучительно писали, причесывали, разбивали на группы и создавали тест кейсы, никуда не годятся. Первая мысль будет - ну его… С этой депрессией необходимо бороться на практике и на этапе написания кода.

Справившись с депрессией, MDD будет для вас хорошей практикой написания кода и тестов с точки зрения применимости к ним разнообразных мутаций еще до запуска самого мутационного тестирования, оно меняет сознание разработчика так же как TDD и BDD.

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

Исторический контекст и определение мутационного тестирования

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

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

ПО может сопротивляться изменениям двумя способами:

в одном нам помогает сам язык - это система типов и компиляция приложения и ваша архитектура.

второе - это тесты на разных уровнях, которые проверяют соответствует ли ПО тем требованиям, которые к нему предъявляют и, если соответствия нет, система проверки должна говорить - нет.

Вопрос к вам? Как вы думаете какой тест лучше зеленый или красный?
Лучше красный, он или неверно написан или словил то, для чего был написан. В зеленый тест вы смотреть не будете никогда. Неважно работает корректно он или нет.

Все на деле просто: вы внесли ошибку, прогнали все тесты, тесты упали - отлично! Система отреагировала правильно.

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

И хорошо если, просто очередной котик на сайте не покажется пользователю.

В итоге мутант может быть убит упавшим тестом - это убитый мутант или ни один тест не среагировал на него - это выживший мутант.

Другим языком убитая мутация это такое условие:

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

Разберем самый простой пример

bool Method(bool a, bool b) {
  if (a && b) { // mutate to ||
    return true;
  } else {
    return false;
  }
}

[Fact]
[InlineData(false, false)]
void Method_ReturnFalse(bool a, bool b) {
  // Act
  var result = Method(a, b);

  // Assert
  Assert.False(result);
}

Мы мутируем && оператор.

Мутант выживет или нет?

Для тогда что бы тест убил мутанта должны быть соблюдены следующие условия:

  1. Тест должен достигнуть кода, то есть покрытие этих строк должно быть 100% - Покрытие
  2. Входные данные для теста должны быть полными, чтобы мутант был убит - проверяются все граничные условия - Тестовые данные
  3. Значение выходных параметров должно влиять на тесты и проверяться в тестах - Достаточная Ассертированность

Вот 3 условия при котором будет убит мутант.

int Method(bool a, bool b) {
  int c;
  if (a && b) { // mutate to ||
    c = 1;
  } else {
    c = 0;
  }
  return c;
}

[Fact]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
void Method_ReturnFalse(bool a, bool b) {
  // Act
  var result = Method(a, b);

  // Assert
  Assert.False(result);
}

Общая модель мутационного тестирования

Как работают системы, которые занимаются мутационным тестированием?

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

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

После этого ваш код собирается со всеми мутациями N раз и результат сборки подмешивается в набор тестов.

Запускаются тесты и падают или не падают.

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

Продолжение в следующей части...

Литература

  1. Assessing Test Quality by David Schuler
  2. Mutation Testing by Filip van Laenen

Ну а если вы любите больше смотреть, чем читать - версия конца 2019 года