Это очередной пост из цикла об этапах написания unit test’а. Для целостности картины, я рекомендую прочитать предыдущий пост. В моих примерах компилятор C# настроен с опцией “treat warnings as errors”. В качестве IDE я использую Visual Studio с установленным ReSharper.
В прошлом посте мы разобрались с тем, как назвать unit test. Теперь мы его заимплементируем. Первым делом расставим явные границы секций Arrange–Act-Assert в виде комментариев:
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange // act // assert }
При разработке ПО важную роль играет погружение в контекст. В моём понимании, это загрузка в оперативную область мозга данных, которые необходимы для того, чтобы писать код, решающий текущую задачу. В подходе, который я опишу тут, контекст для погружения будет минимально глубоким. По сути таким, что прервавшись в любой момент написания теста, можно потом легко продолжить практически мгновенно. Даже если перерыв был длиной в выходные.
Вся хитрость в том, что максимальное погружение в контекст было на предыдущем шаге, когда мы писали название теста. Теперь, если вы отвлеклись на что-то и вернулись к написанию теста, достаточно следовать простейшему алгоритму:
- Если есть ошибки компиляции, исправляем их “в лоб”.
- Если какой-то код лежит не в своём блоке, то перемещаем его в свой блок.
- Находим первый снизу пустой или незаконченный блок Arrange-Act-Assert. Незаконченным является блок, который не полностью соответствует части названия And-When-Then. Если такой блок есть, то кодируем в нём одно условие/действие/проверку из названия теста и переходим к пункту 1.
- Запускаем тест.
- Если тест упал в блоке Arrange, значит в коде самого теста допущена ошибка. С большой вероятностью она лежит на поверхности. Исправляем, переходим к началу пункта 4.
- Если тест упал в блоке Act, значит мы выполнили не достаточную настройку unit under test. Также это звоночек того, что тестируемый класс задизайнен так, что допускает неправильное использование, либо имеет неинтуитивный интерфейс. Дебажим, исправляем, переходим к началу пункта 4.
- Если тест упал в блоке Assert, возможны четыре варианта:
- Ошибка в проверке, т.е. код проверки не соответствует названию теста. Исправляем, переходим к началу пункта 4.
- Настройка unit under test выполнена не полностью. Например, портал надо активировать перед телепортацией. Обязательно дописываем это новое условие в название теста в секцию And и переходим к пункту 1.
- Тестируемый код не написан, т.е. у нас TDD и мы работаем по принципу test first. Пишем код, переходим к началу пункта 4.
- Тестируемый код не работает правильно. Поздравляю, мы нашли баг при помощи теста! Исправляем, переходим к началу пункта 4.
- Если тест не упал, мы закончили.
На самом деле, при должной чистоте кода или хорошем его понимании, пункт 4 будет редким и вся имплементация сведётся к чередованию трёх действий: кодированию названия, “тупому” исправлению ошибок компиляции и двиганию кода в правильные секции.
Посмотрим, как это работает на практике. На старте ошибок компиляции у нас нет. Так же, как нет и кода, который надо расставить по местам. Поэтому делаем сразу пункт 3, то есть ищем первый пустой блок снизу. Это Assert, ему соответсвует часть названия теста Then. Кодируем.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange // act // assert hero. }
В этом месте я обнаруживаю, что IDE не выводит подсказку. Для того, чтобы она появилась, надо объявить переменную hero. Я делаю это средствами ReSharper’а: возвращаю курсор на hero, нажимаю Alt+Enter и выбираю первый пункт “Create local variable”. В дальнейшем большинство действий я буду выполнять через Alt+Enter, ReSharper удивительно точно угадывает нужное мне действие. Итак, мы получаем:
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange // act // assert IHero hero; hero. }
Теперь наличие контекстных подсказок позволяет дописать проверку до конца. Мы ожидаем, что герой потеряет шляпу.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange // act // assert IHero hero; hero.Hat.Should().BeNull(); }
Переходим к шагу исправления ошибок компиляции. У нас такая только одна – значение переменной hero не определено до использования. Исправляем “в лоб”.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange // act // assert IHero hero = new Hero(); hero.Hat.Should().BeNull(); }
Теперь смотрим, какой код находится не в своей секции. Это однозначно строка инициализации героя. Вопрос в том, куда её поместить, в Arrange или Act? Ответ на этот вопрос простой: в Act должен находиться только вызов тестируемого метода. Мы тестируем метод Teleport, а это однозначно не он. Поэтому мы переносим строку инициализации героя в блок Arrange. Это легко сделать комбинацией клавиш Ctrl+Alt+Shift+Вверх.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); // act // assert hero.Hat.Should().BeNull(); }
Снова ищем первую снизу незаполненную секцию. Теперь это Act, а ему соответствует часть заголовка теста When. То есть мы вызовем метод Teleport нашего unit under test.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); // act portal. // assert hero.Hat.Should().BeNull(); }
Как и в прошлый раз, у нас не срабатывает подсказка, потому что переменная portal не определена. Заводим её при помощи Alt+Enter.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); // act Portal portal; portal. // assert hero.Hat.Should().BeNull(); }
Теперь подсказка есть, и мы кодируем телепортирование героя.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); // act Portal portal; portal.Teleport(hero); // assert hero.Hat.Should().BeNull(); }
Переходим к стадии исправления ошибок компиляции. В ней портал обзаведётся инициализацией.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); // act Portal portal = new Portal(); portal.Teleport(hero); // assert hero.Hat.Should().BeNull(); }
Снова стадия перемещения кода, в которой мы видим, что создание портала лежит не в своём блоке. Переносим его из Act в Assert.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero(); Portal portal = new Portal(); // act portal.Teleport(hero); // assert hero.Hat.Should().BeNull(); }
Далее мы ищем первый снизу незаконченный блок. Assert и Act уже закончены, это легко проверить, сопоставляя код блока и соответствующую часть названия теста. Arrange тоже выглядит заполненным, но если его сопоставить с секцией названия теста And, то мы увидим, что шляпу герой ещё не надел. Исправим это.
[Test] public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() { // arrange IHero hero = new Hero { Hat = new Hat() }; Portal portal = new Portal(); // act portal.Teleport(hero); // assert hero.Hat.Should().BeNull(); }
Ошибок компиляции у нас нет. Весь код находится на своих местах. Запускаем тест, убеждаемся, что он зелёный. Поздравляю, с этапом имплементации мы закончили. Я отмечу, что имплементация не подразумевает каких-то дополнительных телодвижений, поэтому рефакторинг теста мы будем проводить на одном из следующих этапов. Пока что ещё рано 🙂
Памятка по соответствию блока теста его названию:
- Arrange – And,
- Act – When,
- Assert – Then.
Памятка по алгоритму имплементации снизу вверх:
- Компилируем;
- Сортируем;
- Заполняем незаконченный блок и переходим к 1;
- Делаем тест зелёным.
На следующем этапе будет проверка теста на ложно-положительность.