Assert DSL

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

По науке, тесты являются документированием системы. Грамотно написанные тесты дают понять, как работает система, как ведет себя, причем читаться все это должно как готовая спецификация на поведение системы. Т.е. в идеале должен получаться связный и понятный текст. Это идеал, к которому постепенно приближаются методы тестирования, начиная от юнит тестирования и наиболее явно проявляясь в поведенческом/приемочном тестировании, когда сами тесты уже пишутся на языке бизнеса (в этом моменте вспоминаем Fitnesse).

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

В общем, все должно быть направлено на максимальную ясность и четкость тестов, чтобы явно было видно все взаимосвязи. Чтобы можно было восстановить логику программы по одним лишь тестам )) В дело читабельности пойдет не только Assert DSL, но и именование файлов, подход Arrange Act Assert. Все это не новые подходы как оказывается, но широкой известности пока не получившие, судя по тому, что я вижу в окружающих меня проектах. Да и сам я натолкнулся на новые темы случайно, изучая исходные коды StructureMap =)

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

  • Именовать тестовые файлы по основному методу, который тестируется
  • Использовать DSL  для создания объектов, чтобы методы делать максимально лаконичными
  • Стараться писать по методу «один метод – один assert»
  • Структурировать внутренности теста
  • Создать и использовать Assert DSL

В широком смысле это укладывается в парадигму Arrange Act Assert, которая говорит о том, что надо четко выделять подготовку к тесту, действие, проверку. В данном случае получится, что каждый тестовый класс будет описывать конкретную подготовку к проведению теста. В SetUp или в FixtureSetUp  будет в идеале задаваться Act и в тестах уже проверятся результат – Assert.

Лучше всего показать это на примере.

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

  • PirateMovementTests
  • PirateAndGoldTests
  • PirateDefaultSettingsTests
  • PirateActionTests

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

  • WhenCreateField
  • WhenCreatePlayableField
  • WhenGenerateShips

После чего тесты внутри класса именовать по примеру:

  • MaxSizeShouldBeDefined
  • ShouldGenerateSeaOnBorder
  • ShouldGenerateShips

Тогда при просмотре тестов и результатов, можно читать их как WhenCreateFiled ShouldGenerateSeaOnBorder –это чистый английский. С пиратами просто чуть больше написать в имени метода, т.е.

  • PirateShouldLostGoldIfHeKilled
  • ActionSurrenderShouldSetPirateOnShip

 

В случае, когда мы можем назвать тестовый класс со слова When, это чистый пример на Arrange Act Assert. Например:

  • Arrange – когда создается игровое поле. В инициализации тестового класса можно подготовить все для создания тестового игрового поля.
  • Act – создание игрового поля. Можно использовать как в самом тестовом методе, так и в инициализации тестового метода.
  • Assert – проверка исполнения. В идеале тестовый метод может только из него и состоять.

Пример:

 

Вся подготовка выполнена в инициализации тестового класса, а дальше идут только проверки.

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

Один тест – одна проверка. Это  можно видеть по примерам выше. Сразу понятно что и как тестируется и должно быть. Часто велик соблазн дописать Assert в уже существующий тест – это называется «дописать зайчика», так вот «зайчики» потом могут сильно аукнуться, так как будут вносить смущения в умы и красть дополнительное время при поднятии тестов после рефакторинга.

Далее очень важна структурированность тестов. Сравните:

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

Еще пример:

 

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

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

Честно сказать, мне такой подход нравится гораздо больше, потому что первым порывом идет написать то свойство, которое было протестировано. Потом уже понимаешь, что надо вписать нужный Assert, на которые я раньше вешал сокращения для InteliSense. Например для Assert.AreEqual / Assert.That( $actual$, Is.EqualTo($expected$))  было aae, т.е. набрал эту комбинацию, нажал Tab и уже есть шаблон в коде. Но это неудобно, это надо было настраивать ReSharper.

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

Сравните два подхода:

 

И еще пример:

 

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

 

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

 

Какой способ понятнее?

 

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

 

 

 

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

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

 

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

 

Hard’n’heavy!

 

Оставить комментарий