MSTest V2 – Часть 1: Проверяем результат.

.NET logoПри создании тестов с использованием библиотеки MSTest V2 не редко используются только её основные возможности для проверки результата. Это приводит к гораздо большему объему написанного кода и созданию очередных "велосипедов". Посмотрим как можно этого избежать и какая функциональность для проверки результатов тестов есть "из коробки" в данной библиотеке.

Обычный проект с тестами

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

Сам проект можно создать:

  • В Visual Studio, выбрав на этапе создания проекта шаблон "MSTest Test Project" (или "Unit Test Project" для .NET Framework).
  • Выполнив в командной строке dotnet new mstest.
  • Создав проект библиотеки классов "Class Library" и добавив в него следующие три NuGet пакета: Microsoft.NET.Test.SdkMSTest.TestAdapter, MSTest.TestFramework.

Следующим шагом будет добавление классов с методами, в которых и будет расположен код тестов. Имена классов могут быть произвольными, но объявленные типы должны быть отмечены атрибутом [TestClass].

Методы с тестами могут быть как синхронными (и возвращать void), так и асинхронными (должны иметь ключевое слово async и возвращать Task). При этом каждый из них отмечается специальным атрибутом [TestMethod]. Тест считается успешным, если он завершился без исключения или с исключением которое было ожидаемо.

Для кода тестов отлично подходит шаблон AAA (Arrange, Act, Assert). Он подразумевает логическое разделение кода теста на три части:

  1. Arrange – инициализация данных, создание как необходимых объектов, включая объект для тестирования.
  2. Act – непосредственно выполнение теста (как правило вызов тестируемого метода).
  3. Assert – проверка полученных результатов.

Для большего удобства сопровождения тестов код можно разделить еще и визуально, используя комментарии.

В последней части, где проверяется результат теста, как правило используются методы статического класса Assert. Наверное, это самый часто используемый класс библиотеки. Вызовы его методов IsNull / IsNotNull, AreEqual / AreNotEqual, IsTrue / IsFalse можно найти практически в каждом тесте. Чуть реже встречаются IsInstanceOfType, AreSame / AreNotSame.

Но какие еще возможности предоставляет MSTest V2 для проверки результатов?

Проверка результата

Assert

Посмотрим внимательнее на класс Assert. Стоит ещё отметить несколько его редко используемых методов:

  • Assert.ThrowsException<T>(Action action) – проверяет что переданный делегат выбрасывает исключение указанного типа (не включая его наследников).
  • Assert.ThrowsExceptionAsync<T>(Func<Task> action) – аналогичен описанному выше, но для асинхронных делегатов.
  • Assert.Fail(string message) – завершает текущий тест с ошибкой и указанным сообщением.
  • Assert.Inconclusive(string message) – позволяет пропустить текущий тест (будет отмечен как пропущенный).

С помощью Assert.Inconclusive(…) можно пропускать тесты в зависимости, например, от операционной системы, переменных окружения и других внешних факторов.

Однако, на Assert функциональность библиотеки не заканчивается. Посмотрим еще на несколько классов, позволяющих не заниматься созданием "велосипедов".

StringAssert

Класс StringAssert, как можно догадаться, предоставляет методы для проверки строк, которые завершаются успешно если указанная строка:

  • Matches(…) – соответствует регулярному выражению.
  • DoesNotMatch(…)  – не соответствует регулярному выражению.
  • StartsWith(…) – начинается с заданной подстроки.
  • EndsWith(…)  – заканчивается заданной подстрокой.
  • Contains(…)  – содержит заданную подстроку.

CollectionAssert

Класс CollectionAssert содержит методы для проверки коллекций, которые реализуют интерфейс ICollection. Стоит отметить, что для сравнения элементов используется метод Equals(Object, Object).

Методы рассматриваемого класса проверяют что:

  • AllItemsAreInstancesOfType(…) – все элементы коллекции являются экземплярами указанного типа или его наследников.
  • AllItemsAreNotNull(…) – все элементы коллекции не равны null.
  • AllItemsAreUnique(…) – все элементы коллекции уникальные (нет двух равных друг другу элементов)
  • Contains(…) – коллекция содержит указанный элемент.
  • DoesNotContain(…) – коллекция не содержит указанный элемент.
  • AreEqual(…) – две коллекции равны, то есть одинаковой длины и содержат одинаковые элементы на одинаковых позициях.
  • AreNotEqual(…) – две коллекции не равны.
  • AreEquivalent(…) – две коллекции эквиваленты, то есть одинаковой длины и содержат одинаковые элементы не зависимо от того, на каких они позициях.
  • AreNotEquivalent(…) – две коллекции не эквиваленты.
  • IsSubsetOf(…) – одна коллекция является подмножеством другой (то есть все элементы первой коллекции входят во вторую).
  • IsNotSubsetOf(…) – одна коллекция не является подмножеством другой.

Добавляем свои проверки

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

  • Как сделать так, чтобы другим разработчикам было проще найти и использовать новый метод проверки?
  • Что если кто-то в другом написанном им тесте будет ожидать исключение того же типа (и тогда проверка всегда будет ложно-позитивной)?

Создатели MSTest V2 предлагают следующий путь:

  • У каждого из рассмотренных выше классов есть свойство That, в котором находится экземпляр данного класса. Это позволяет написать метод-расширение, которое будет доступно в подсказках (IntelliSense в Visual Studio и т. д.).
  • В случае неудачной проверки нужно выбрасывать специальное исключение AssertFailedException. Кстати, его используют описанные выше методы.

Например:

public static class AssertExtensions 
{
    public static void IsValidResponse(this Assert assert, Response response, string errorMessage)
    {
        // Логика проверки response…
    }
}

В таких методах хорошей практикой является наличие параметра errorMessage для указания необходимого сообщения об ошибке.

Теперь вызовы будут выглядеть так:

Assert.That.IsValidResponse(response, "Получен некорректный response."); 

Проверяем исключения

Выброс исключения может быть ожидаемой ситуацией в тестах, когда, например, проверяется код валидации переданных в сервис данных. Конечно, можно воспользоваться try/catch и сравнить пойманное исключение с ожидаемым. Но есть способ проще: у метода теста указать атрибут ExpectedException(…), передав в качестве параметра тип ожидаемого исключения.

[TestMethod]
[ExpectedException(typeof(ValidationException))]
public async Task UserService_AddUserWithoutName_Failed()
{
    // Arrange
    var invalidUser = new User( … );
    var service = new UserService( … );

    // Act
    await service.AddUser(invalidUser);

    // Assert
}

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

В данном подходе можно заметить одну проблему: нет возможности проверить что лежит внутри исключения. Если это необходимо сделать 1 – 2 раза, то можно использовать try/catch. Но в других случаях удобнее будет реализовать свой атрибут. Для этого нужно:

  • Создать свой класс, унаследовав его от ExpectedExceptionBaseAttribute.
  • Реализовать метод Verify по следующему правилу: если полученное исключение ожидаемое, то необходимо просто завершить работу метода (то есть, по сути, ожидаемое исключение “глушится”). В противном случае достаточно выкинуть любое исключение с сообщением, описывающим найденную проблему.

Рассмотрим на упрощенном примере. Допустим есть исключение ValidationException, сигнализирующее об ошибках валидации и хранящее их список в свойстве Errors.

public interface IValidationError { }
public class ArgumentMissingValidationError : IValidationError { … }
public class ArgumentOutOfRangeValidationError : IValidationError { … }

public class ValidationException : Exception
{
    public IReadOnlyCollection Errors { get; }

    public ValidationException(params IValidationError[] errors) 
        => Errors = errors;
}

Реализуем атрибут, который будет проверять, что в выброшенном исключении содержится указанная ошибка.

public class ExpectedValidationExceptionAttribute : ExpectedExceptionBaseAttribute
{
    private readonly Type _validationErrorType;

    public ExpectedValidationExceptionAttribute(Type validationErrorType)
        => _validationErrorType = validationErrorType;

    protected override void Verify(Exception exception)
    {
        if (exception is not ValidationException e)
            throw new Exception("Ожидаемое исключение не было выброшено.");

        if (e.Errors.Any(x => x.GetType() == _validationErrorType))
            throw new Exception("Заданная ошибка валидации не была найдена.");
    }
}

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

[TestMethod]
[ExpectedValidationExceptionAttribute(typeof(ArgumentMissingValidationError))]
public async Task UserService_AddUserWithoutName_Failed()
{
    // Arrange
    var invalidUser = new User( … );
    var service = new UserService( … );

    // Act
    await service.AddUser(invalidUser);

    // Assert
}

В следующей части разберем какие еще возможности предоставляет библиотека MSTest V2.

Комментарии (4) -

Зачем нужен сегодня MSTest когда есть xUnit, NUnit ?

Для нового проекта есть выбор (причем учитывая опыт команды). И MSTest вполне себе нормальный интрумент для тестов.

Плюс есть куча уже существующих проектов c MSTest, где тесты надо поддерживать и добавлять новые.

Собственно статистика загрузок на NuGet об этом и говорит (MSTest выглядит вполне достойно).

Если уже есть проекты, то конечно лучше взять то, что есть.
Кстати какое мнение насчёт FluentAssertions или Shoudlify ?
Я вот подумываю перейти, чтобы быть независимым от тестового фреймворка.

FluentAssertions в реальных проектах не использовал. Выглядит интересно, но вот стал бы использовать в реальном проекте - большой вопрос. За все время работы я как-то не сталкивался с проблемой сообщений от тестов. Но есть привычка, в assert всегда коротко писать свое сообщение. Ну и как правило, если тест "красный", то просто проходишся по нему отладчиком + live unit testing.

Опять же, если смотреть как на абстракцию от конкретной test library - а оно насколько нужно (тем более как я понимаю это не их главная цель)? Как часто меняется test library на проекте? При выходе какой-то супер фичи в одной из них сколько я готов ждать пока она появится в абстракции?

В общем, вот поэтому есть большие сомнения.

Добавить комментарий