Статьи

Улучшение модульного тестирования с помощью FluentAssertions

FluentAssertions — это альтернативная библиотека утверждений для модульных тестов, которую следует использовать вместо методов в классе Assert, которые предоставляет Microsoft. У него гораздо лучшая поддержка исключений и некоторых других вещей, которые улучшают читаемость и облегчают создание тестов.

Кодирование Kentor.AuthServices стало для меня прекрасной возможностью снова сделать настоящий TDD (Test Driven Development). Я долго думал, что [ExpectedException]атрибута, который предлагает MsTest, недостаточно, поэтому, когда Альбин Суннанбо предложил мне посмотреть на FluentAssertions, я решил попробовать его.

Проверка исключений

FluentAssertions предлагает ShouldThrow()метод расширения для Actionтипа делегата. Он утверждает, что вызов определенного действия вызовет исключение.

// Code from https://github.com/KentorIT/authservices/blob/master/
// Kentor.AuthServices.Tests/Saml2ResponseTests.cs
Action a = () => Saml2Response.Read(response).GetClaims();
 
a.ShouldThrow<InvalidOperationException>()
.WithMessage("The Saml2Response must be validated first.");

По сравнению с [ExpectedException]атрибутом это предлагает намного лучший контроль.

Проблема с [ExpectedException]

Проблема в [ExpectedException]том, что в нем отсутствует важное свойство: точное указание, где ожидается исключение. Рассмотрим тест, который сначала захватывает подходящий объект для тестирования с помощью LINQ, а затем вызывает исключение.

[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Using_ExpectedException()
{
  var attributes = typeof(Brand).GetMember("Nmae").Single()
    .GetCustomAttributes(false);
 
  // This should throw.
  attributes.OfType<RequiredAttribute>().First()
    .FormatErrorMessage("InvalidName");
}

При запуске этот тест будет отмечен зеленым, несмотря на опечатку в GetMemberвызове. Проблема в том, что GetMemberвыдает InvalidOperationExceptionметод, который является именно тем, что требуется для прохождения метода. Там нет никакого способа , с , [ExpectedException]чтобы убедиться , что исключение в нужном месте. Также нет способа проверить более чем одну вещь (например, пост-условие, что объект находится в согласованном состоянии после того, как метод вызвал исключение).

Вместо этого используйте FluentAssertions

С FluentAssertions это намного лучше.

[TestMethod]
public void Test_Using_FluentAssertions()
{
    var attributes = typeof(Brand).GetMember("Nmae").Single()
        .GetCustomAttributes(false);
 
    Action a = () => attributes.OfType<RequiredAttribute>().First()
        .FormatErrorMessage("InvalidName");
 
    a.ShouldThrow<InvalidOperationException>();
}

Код для проверки на исключение теперь помещен в действие. Метод ShouldThrow()extension выполняет действие и отслеживает исключение. Тест теперь не проходит должным образом, потому что GetMember("Nmae").Single()выдает исключение, которое не ожидается.

Больше FluentAssertions

Основной синтаксис FluentAssertions заключается в использовании нескольких методов расширения, которые расширяют все. Это еще один пример от Kentor.AuthServices.

// From https://github.com/KentorIT/authservices/blob/master/
// Kentor.AuthServices.Tests/IdentityProviderTests.cs
var r = ip.CreateAuthenticateRequest();
 
r.ToXElement().Attribute("Destination").Should().NotBeNull()
   .And.Subject.Value.Should().Be(idpUri);

Здесь я могу использовать одну строку, чтобы проверить, что результат ToXElement()имеет Destinationатрибут и что значение атрибута является правильным.

Лучшее: ShouldBeEquivalentTo

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

var expected = new
{
  Id = "Saml2Response_Read_BasicParams",
  IssueInstant = new DateTime(2013, 01, 01, 0, 0, 0, DateTimeKind.Utc),
  Status = Saml2StatusCode.Requester,
  Issuer = (string)null
};
 
Saml2Response.Read(response).ShouldBeEquivalentTo(expected);

К сожалению, в API есть недостаток (или я не научился правильно его использовать). ShouldBeEquivalentToтолько проверяет свойства, которые присутствуют в предмете теста, и игнорирует любые дополнительные свойства, найденные в ожидаемом объекте. Я бы предпочел иметь это наоборот. Таким образом, я мог бы ввести новое свойство, начав с добавления его к ожидаемому объекту в тесте.

В реальной жизни, однако, это не большая проблема — просто добавьте новое свойство с return null;геттером к тестируемому объекту, и тест провалится. Но если бы я мог чего-то пожелать, я бы хотел, чтобы FluentAssertions был расширен с таким поведением, по крайней мере, в качестве настраиваемой опции.

Установка FluentAssertions

Начать работу с FluentAssertions чрезвычайно легко . Просто добавьте пакет FluentAssertions Nuget в проект и затем включите его using FluentAssertions;в тестовый файл.

Установка FluentAssertions настолько проста, потому что это просто еще один способ написания утверждений теста — он не меняет инфраструктуру теста. Это означает, что тестовый прогон (который может потребовать отдельной установки) не затронут. Это большое преимущество, поскольку необходимость установки отдельных тестовых программ в Visual Studio для нового проекта — это настоящая боль.