И снова здравствуйте. В преддверии старта курса «Разработчик C#» перевели интересный материал про assert-сообщения в тестах и с радостью делимся с вами переводом.




В этом посте мы поговорим о том, должны ли вы использовать Assert-сообщения в ваших тестах.

Я получил интересный вопрос от коллеги читателя, на котором хотел бы остановиться поподробнее:

У меня вопрос по поводу Assert-сообщений: следует ли использовать перегрузку, содержащую параметр сообщения, и использовать ее для передачи строки, описывающей причину неудачи Assert (также “Утверждения”)?

Ответ на этот вопрос сводится к двум аспектам:

  • Читаемость теста — насколько легко понять, что делает тест.
  • Простота диагностики — насколько легко понять, почему тест не пройден.

Давайте обсудим каждый из них в отдельности

Читаемость теста


Люди часто используют Assert-сообщения, чтобы помочь членам команды и самим себе в будущем понять, что происходит в тесте. Давайте рассмотрим следующий пример:

[Test]
public void Hiring_a_new_team_member()
{
    var company = new Company();
    var person = new Person(UserType.Customer);

    company.HireNewMember(person);

    Assert.AreEqual(UserType.Employee, person.Type); // нет сообщения
    Assert.AreEqual(1, company.NumberOfEmployees); // нет сообщения
}

Вместо голого Assert вы также можете указать причину, по которой тестовый Assert что-либо валидирует:

[Test]
public void Hiring_a_new_team_member()
{
    var company = new Company();
    var person = new Person(UserType.Customer);

    company.HireNewMember(person);

    Assert.AreEqual(UserType.Employee, person.Type, "Person must become an employee after hiring");
    Assert.AreEqual(1, company.NumberOfEmployees, "Number of employees must increase");
}

Такие утверждения помогают, но они имеют свою цену. Эти сообщения требуют от вас

  • Потратить время на их написание
  • Поддерживать их продвижение вперед

Здесь набор плюсов и минусов такой же, как и в комментариях к коду. И, как и в случае с комментариями, я советую вам: не пишите assert-сообщения чисто в целях читаемости теста. Если вы чувствуете, что тест не очевиден без assert-сообщений, попробуйте вместо этого провести его рефакторинг. В долгосрочной перспективе легче сделать тест говорящим самим за себя, нежели поддерживать его синхронизацию с assert-сообщениями (и это только вопрос времени, когда синхронизация будет нарушена).

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

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

[Test]
public void Hiring_a_new_team_member()
{
    var company = new Company();
    var person = new Person(UserType.Customer);

    company.HireNewMember(person);

    Assert.That(person.Type, Is.EqualTo(UserType.Employee));
    Assert.That(company.NumberOfEmployees, Is.EqualTo(1));
}

Или вы можете использовать мои любимые Fluent Assertions:

[Test]
public void Hiring_a_new_team_member()
{
    var company = new Company();
    var person = new Person(UserType.Customer);

    company.HireNewMember(person);

    person.Type.Should().Be(UserType.Employee);
    company.NumberOfEmployees.Should().Be(1);
}

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

[Субъект] [действие] [объект].

Например,

Боб открыл дверь.

Здесь Боб — субъект, открыл — действие, а дверь — объект. То же самое относится и к коду.

Эта версия

company.NumberOfEmployees.Should().Be(1);

читает лучше чем

Assert.AreEqual(1, company.NumberOfEmployees);

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

Простота диагностики


Другой взгляд на assert-сообщения — с точки зрения простоты диагностики. Другими словами, простота понимания причины провала теста без изучения кода этого теста. Это полезно при чтении результатов сборки CI.

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


Пирамида тестирования

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

Но даже с интеграцией и сквозными тестами существуют способы облегчить диагностику, не прибегая к assert-сообщениям:

  • Сделайте так, чтобы тест проверял один модуль поведения — когда тест проверяет что-то одно, часто легко определить, что пошло не так. (Это не всегда применимо к сквозным тестам, так как вы можете захотеть, чтобы такие тесты проверяли, как несколько единиц поведения работают вплотную).
  • Именуйте свои модульные тесты соответственно — Идеальное имя теста описывает поведение приложения в бизнес-терминах, так что даже непрограммист может его понять.

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

Например, ошибка в следующем утверждении:

person.Type.Should().Be(UserType.Employee);

выдает следующее сообщение об ошибке:

Xunit.Sdk.EqualException: Assert.Equal() Failure
Expected: Employee
Actual:   Customer

Комбинация таких генерируемых средой сообщений и понятных человеку имен тестов делает 90% пользовательских assert-сообщений бесполезными даже с точки зрения простоты диагностики. Единственное исключение — это длительные сквозные тесты. Они часто содержат многоэтапные проверки, поэтому имеет смысл использовать дополнительные assert-сообщения, чтобы понять, какой из шагов не удался. Однако таких сквозных тестов не должно быть много.

Конечно, чтобы воспользоваться сгенерированными фреймворком сообщениями о сбоях, вам нужно избегать общих булевых сравнений, таких как:

(person.Type == UserType.Employee).Should().BeTrue();

Потому что они приводят к следующему сообщению об ошибке:

Xunit.Sdk.TrueException: Assert.True() Failure
Expected: True
Actual:   False

Что не помогает с диагностикой вообще.

Резюме


Вы часто ощущаете лень, когда речь идет о написании утверждений? Что ж, теперь вы можете оправдать ее и отослать своих коллег к этой статье.


«Я рад, что у этого есть название»

Шутки в сторону, однако, вот резюме:

  • Существует два аспекта использования assert-сообщений:
    • Читаемость теста (насколько легко понять, что делает тест).
    • Простота диагностики (насколько легко понять, почему тест не проходится во время сборки CI).
  • С точки зрения читабельности теста assert-сообщения являются комментариями кода. Вместо того чтобы полагаться на них, займитесь рефакторингом теста для достижения читаемости.
  • С точки зрения простоты диагностики, лучшая альтернатива assert-сообщениям:
    • Проверка тестом одного модуля поведения
    • Именование тестов в бизнес терминах
  • Единственное исключение — длительные сквозные тесты.

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