Ложно-отрицательные unit тесты

Этот пост продолжает цикл этапов написания unit test’а. Он является полностью независимым и не требует прочтения предыдущих, но я рекомендую почитать хотя бы про ложно-положительные тесты. В примерах используется NUnit и FluentAssertions.

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

Дадим определение. Ложно-отрицательным является тест, который падает, но при этом логика, которую тест должен проверять, работает правильно. Грубо говоря такой тест – это как мальчик в сказке, который кричал “Волки!” в шутку, и ему в конце концов перестали верить.

В шутку тесты мы не пишем, но ложно-отрицательные тесты всё равно могут получаться. Я покажу это на примере.

Тестируем портал между измерениями. По логике приложения портал не пропускает мёртвых героев. В случае, когда мёртвый герой пытается пройти сквозь портал, вылетает exception с сообщением “Hero dead”. Ниже приведён сам unit under test.

public class Portal
{
    public void Teleport(IHero hero)
    {
        if (!this.IsActivated)
            throw new Exception("Portal is not activated");

        if (hero.IsDead)
            throw new Exception("Hero dead");
    }
}

Как мы видим, если портал не был активирован, тоже вылетает exception. Получается, что в тесте проверки только на наличие exception будет недостаточно. Потому что мы хотим убедиться, что сработала именно логика, проверяющая мёртвого героя. Ниже приведён тест, проверяющий эту логику.

public void WhenTeleportingHero_AndHeroIsDead_ThenShouldThrowHeroDead()
{
    // arrange
    var hero = new Hero { IsDead = true };
    var portal = new Portal();

    // act
    var exception = CatchException(() => portal.Teleport(hero));

    // assert
    exception?.Message.Should().Be("Hero dead");
}

Здесь CatchException – это самописный метод, который возвращает вылетевший exception или null, если всё прошло без ошибок.

Этот тест прошёл все этапы написания unit test’а, кроме проверки на ложно-отрицательность. Программист, который его написал, проделал аккуратную работу, закоммитил код, убедился, что continuous integration билд прошёл успешно, и, довольный собой, занялся следующей задачей.

Дни сменялись ночами, с деревьев опала листва, выпал снег и, когда прорезались подснежники, за код портала взялся другой сотрудник. Одной из его задач было добавить новый функционал. А заодно он увидел, что “Hero dead” написано неправильно с точки зрения правил английского языка. И исправил это.

public class Portal
{
    public void Teleport(IHero hero)
    {
        if (!this.IsActivated)
            throw new Exception("Portal is not activated");

        if (hero.IsDead)
            throw new Exception("Hero is dead");
    }
}

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

public void WhenTeleportingHero_AndHeroIsDead_ThenShouldThrowHeroDead()
{
    // arrange
    var hero = new Hero { IsDead = true };
    var portal = new Portal();

    // act
    var exception = CatchException(() => portal.Teleport(hero));

    // assert
    exception?.Message.ToLower().Should().Contain("hero", "dead");
}

Такой тест не упадёт в новой версии кода. И даже при более интересном сообщении вроде “Dead heroes are not allowed to go through portal”. Также обратите внимание, что проверка регистронезависимая.

Таким образом, после имплементации теста, для недопущения ложных падений, его надо проверить на ложно-отрицательность. К сожалению, в проверке на ложно-отрицательность нет готового алгоритма, как был в проверке на ложно-положительность. Тут придётся немного подумать 🙂

Надо посмотреть очень внимательно на тест и спросить себя, кажется ли он хрупким? И если да, то в каких местах? Когда такие места обнаружены, надо придумать как их исправить. Я постараюсь привести пример, и на нём показать, в каком направлении можно думать.

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

Тест на восстановление здоровья после шага имплементации будет выглядеть так:

public void WhenTeleportingHero_AndHeroHasMaxLevel_AndHeroHasHalfHP_ThenHeroHPShouldRestoreToFull()
{
    // arrange
    var hero = new Hero { Level = 80, HealthPercent = 50 };
    var portal = new Portal();

    // act
    portal.Teleport(hero);

    // assert
    hero.HealthPercent.Should().Be(100);
}

Попробуем поверить его на ложно-отрицательность и посмотрим в каких местах тест является хрупким. Я рекомендую обращать внимание на магические константы. В нашем случае это 80, 50 и 100. 50 соответствует половине HP в процентах и с этим всё в порядке. Со значением 100 тоже всё хорошо, т.к. 100% это и есть максимум здоровья. А вот 80 мы приняли за максимальный уровень, что не всегда может быть таковым. Если максимальный уровень станет 90, то мы получим ложно-отрицательное срабатывание этого теста. Давайте это исправим.

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

public void WhenTeleportingHero_AndHeroHasMaxLevel_AndHeroHasHalfHP_ThenHeroHPShouldRestoreToFull()
{
    // arrange
    var hero = new Hero { Level = Hero.MaxLevel, HealthPercent = 50 };
    var portal = new Portal();

    // act
    portal.Teleport(hero);

    // assert
    hero.HealthPercent.Should().Be(100);
}

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

Для того, чтобы понять, как поступить, проведём мысленный эксперимент. Чем плоха эта хрупкость теста? Допустим у нас есть 22 теста, которые так или иначе завязаны на то, что максимальный уровень героя – 80. Как только правила игры изменятся и максимальный уровень станет 90, то всё 22 теста станут красными и тому, кто имплементирует повышение героя, придётся тратить время на каждый тест, чтобы понять что он всего лишь ложно-отрицательный.

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

public void WhenTeleportingHero_AndHeroHasMaxLevel_AndHeroHasHalfHP_ThenHeroHPShouldRestoreToFull()
{
    // arrange
    var hero = new Hero { Level = Balance.MaxHeroLevel, HealthPercent = 50 };
    var portal = new Portal();

    // act
    portal.Teleport(hero);

    // assert
    hero.HealthPercent.Should().Be(100);
}

Здесь Balance – это класс, который лежит в сборке с тестами и содержит константы, которые зависят от игрового баланса. В идеале такого рода классов должно быть немного и они должны лежать в корне сборки, чтобы их легко было найти и повторно использовать в других тестах.

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

Подведём итоги. Цель проверки на ложно-отрицательность – гарантировать отсутствие ложных падений в случае, если тестируемая логика не менялась. Если гарантировать отсутствие ложных срабатываний не возможно, то надо минимизировать вред от этих таких тестов. Идеальное минимизирование – когда после исправления одного теста все остальные исправляются автоматически без их открытия и редактирования. Простого алгоритма проверки нет, требуется привлечение умственных ресурсов разработчика, а именно анализ теста на “хрупкость”.

В следующем посте мы поговорим о рефакторинге теста перед отправкой его во взрослую жизнь 🙂

Ложно-положительные unit тесты

Этот пост продолжает цикл этапов написания unit test’а. Он является полностью независимым и не требует прочтения предыдущих. В примерах используется NUnit и FluentAssertions.

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

Возникает резонный вопрос, какой вообще смысл в таком тесте? Честно говоря, никакого. Казалось бы, о чём мы вообще тут говорим? Ведь всё просто, достаточно не писать ложно-положительные тесты, т.к. в них нет смысла. Проблема в том, что часто по виду теста нельзя понять, является ли он ложно-положительным. Рассмотрим на примере.

У нас есть портал. Над ним сияет надпись, которая предупреждает о необходимости сохранять молчание в другом измерении, чтобы не навлечь на себя гнев Древних. Мы хотим проверить в тесте, что после деактивации портала надпись пропадает.

Вот наш unit under test, точнее важная для нас его часть:

public class Portal
{
    public string Inscription { get; private set; } = "Thou shalt not talk!";

    public void Deactivate()
    {
        this.Inscription = null;
    }
}

А вот unit test, который проверяет эту логику:

[Test]
public void WhenDeactivatingPortal_ThenInscriptionShouldDisappear()
{
    // arrange
    var portal = new Portal();

    // act
    portal.Deactivate();

    // assert
    portal.Inscription.IsNullOrEmpty();
}

На первый взгляд всё в порядке. Тест зелёный, сдан в репозиторий и успешно проходит во время continuous integration билда. Прошло время, и в один солнечный день некий разработчик решает, что выключенный портал без надписи это слишком скучно, и меняет код на своё усмотрение:

public class Portal
{
    public string Inscription { get; private set; } = "Thou shalt not talk!";

    public void Deactivate()
    {
        this.Inscription = "Under maintenance";
    }
}

Разработчик не подумал о том, что на выключенный портал код отрисовки надписи не рассчитан и приложение бы в этом месте упало. Однако же у нас есть unit test, который должен был упасть на билд сервере и не дать пролезть багу в систему. Однако этого не произошло. Я думаю, что если тест был бы написан не на C#, а на японском, он бы покончил с собой, потому что в ключевой момент не исполнил цель всей своей жизни.

Давайте разберёмся, почему так произошло. Всё дело в последнем методе IsNullOrEmpty(). Это обычный extension метод, который делает вызов метода string.IsNullOrEmpty() чуть более удобным. В случае отрицательного результата он возвращает false, который просто игнорируется тестом.

public static class Extensions
{
    public bool IsNullOrEmpty(this string value)
    {
        return string.IsNullOrEmpty(value);
    }
}

Видимо кто-то, кто написал тест, банально перепутал этот метод с вызовом реальной проверки из assertion framework’а. Правильная проверка должна выглядеть так:

[Test]
public void WhenDeactivatingPortal_ThenInscriptionShouldDisappear()
{
    // arrange
    var portal = new Portal();

    // act
    portal.Deactivate();

    // assert
    portal.Inscription.Should().BeNullOrEmpty();
}

Как видите, на этом тесте не было очевидно, что он ложно-положителен. В реальности ситуации могут быть ещё более запутанными, и далеко не всегда заметить подобные ошибки можно невооруженным взглядом. Поэтому после имплементации очень важным шагом является проверка теста на ложно-положительность.

Алгоритм такой проверки прост. Достаточно пойти в реализацию тестируемого метода и изменить её так, чтобы проверяемая логика стала работать наоборот или перестала работать вообще.

В нашем примере достаточно убрать код, который стирает надпись.

public class Portal
{
    public string Inscription { get; private set; } = "Thou shalt not talk!";

    public void Deactivate()
    {
    }
}

Если тест упал, то теперь надо убедиться, что он упал с ожидаемым сообщением. Если так и есть, то проверка на ложно-положительность пройдена успешно. Если тест не упал или упал с неправильным сообщением, например с “Object reference not set to an instance of an object”, то тест проверку не прошёл и надо его исправлять. Чаще всего такие исправления сосредоточены в блоке Assert.

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

public class Portal
{
    public string Inscription { get; private set; } = "Thou shalt not talk!";

    public void Deactivate()
    {
        this.Inscription = null;
    }
}

Кстати, не бойтесь что вы забудете поменять код назад. У вас будет красный тест, который упадёт и напомнит 🙂

В следующем посте мы поговорим о том, как проверить тест на ложно-отрицательность.

 

Имплементация снизу вверх – второй этап написания 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. Делаем тест зелёным.

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

 

Именование – первый этап написания unit test’а

Этот пост – первый из цикла об этапах написания unit test’а.

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

Времена тестов Record-Repeat ушли, и сейчас шире используется структура Arrange-Act-Assert. Это мы и отразим в тесте, назвав его по шаблону WhenAct_AndArrange_ThenAssert. Наша цель – выбрать последовательно, какие осмысленные действия будут происходить в Act, Arrange и Assert и закрепить их в названии.

Разберём каждый шаг на примере. Нашим unit under test будет портал, который телепортирует героев между измерениями. Тестировать мы будем следующий функционал: Когда герой в шляпе проходит в другое изменение, резкий порыв ветра срывает с него шляпу и он её теряет.

Вот часть интерфейса портала, которая важна нам в этой статье:

public class Portal
{
    public void Teleport(IHero hero) {}
}

Как видите, я сознательно не указываю реализацию, т.к. сейчас она для нас интереса не представляет.

Act

Определимся с тестируемым методом. В данном случае это Teleport. Действие, которое он выполняет – телепортация героя. Запишем это в заголовок теста во времени present continuous “when teleporting hero..”.

Название нашего теста станет:

[Test]
public void WhenTeleportingHero_...() {}

Present continuous тут не совсем подходит с точки зрения правил написания английских предложений. Однако это даёт нам бонус в том, что алфавитная сортировка тестов совпадает с алфавитной сортировкой методов unit under test. Это очень упрощает навигацию по тестам и методам класса и даёт возможность получить представление о поведении класса глядя на класс и его тесты “с высоты птичьего полёта”, т.е. когда все методы схлопнуты.

public class Portal
{
    public void Activate() {}
    public void Deactivate() {}
    public void Teleport(IHero hero) {}
}
public class PortalTests
{
    [Test]
    public void WhenActivating…() {}

    [Test]
    public void WhenDeactivating…() {}

    [Test]
    public void WhenTeleporingHero…() {}
}

Arrange

Секция Arrange в тесте отвечает за дополнительные условия, которые не являются логическим умолчанием. Грубо говоря это какие-то отклонения в настройке unit under test или необычные входные данные. В данном примере наш unit under test работает в штатном режиме, однако есть дополнительное условие на герое: он в шапке – “hero wears hat”.

Это и продолжит название нашего теста:

[Test]
public void WhenTeleportingHero_AndHeroWearsHat_...() {}

Сразу упомяну, что когда таких специальных условий несколько (например, герой в шляпе упрямо пытается пройти в деактивированный портал), то, для облегчения жизни нашего гипотетического программиста из будущего, всё из этого надо отразить в названии теста:

[Test]
public void WhenTeleportingHero_AndHeroWearsHat_AndPortalIsDeactivated_...() {}

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

Assert

Эта секция теста содержит набор проверок правильности логики работы unit under test. В нашем примере мы ожидаем, что герой потеряет свою шапку – “hero should lose hat”.

Название нашего теста полностью готово:

[Test]
public void WhenTeleportingHero_AndHeroWearsHat_ThenHeroShouldLoseHat() {}

Теперь дело за имплементацией 🙂

Этапы написания unit test’а (для новичков*)

* и не только 🙂

Этот пост открывает серию, посвящённую стадиям, которые я считаю неотъемлемыми при написании unit test’а. Каждый этап будет подробно разобран в отдельном посте, а список этапов постепенно превратится в список ссылок.

Итак, написание unit test’а состоит из следующих этапов:

  1. Именование
  2. Имплементация снизу-вверх
  3. Проверка на ложно-положительность
  4. Проверка на ложно-отрицательность
  5. Рефакторинг
  6. Проверка и упрощение именования

Disclaimer

По шкале овладевания навыками Су-Ха-Ри, статьи будут максимально полезны для тех, кто находится на стадии “Су”. Однако я ожидаю, что вы уже понимаете, что такое unit тестирование и для чего оно нужно, а также владеете базовым пониманием структуры теста и mocking/isolation фреймворков.