Этот пост продолжает цикл этапов написания 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; } }
Кстати, не бойтесь что вы забудете поменять код назад. У вас будет красный тест, который упадёт и напомнит 🙂
В следующем посте мы поговорим о том, как проверить тест на ложно-отрицательность.