Имплементация снизу вверх – второй этап написания unit test’а

Это очередной пост из цикла об этапах написания unit test’а. Для целостности картины, я рекомендую прочитать предыдущий пост. В моих примерах компилятор C# настроен с опцией “treat warnings as errors”. В качестве IDE я использую Visual Studio с установленным ReSharper.

В прошлом посте мы разобрались с тем, как назвать unit test. Теперь мы его заимплементируем. Первым делом расставим явные границы секций ArrangeAct-Assert в виде комментариев:

[Test]
public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat()
{
   // arrange

   // act

   // assert

}

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

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

  1. Если есть ошибки компиляции, исправляем их “в лоб”.
  2. Если какой-то код лежит не в своём блоке, то перемещаем его в свой блок.
  3. Находим первый снизу пустой или незаконченный блок Arrange-Act-Assert. Незаконченным является блок, который не полностью соответствует части названия And-When-Then. Если такой блок есть, то кодируем в нём одно условие/действие/проверку из названия теста и переходим к пункту 1.
  4. Запускаем тест.
    • Если тест упал в блоке 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. Компилируем;
  2. Сортируем;
  3. Заполняем незаконченный блок и переходим к 1;
  4. Делаем тест зелёным.

На следующем этапе будет проверка теста на ложно-положительность.

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s