Автор: Sergey Teplyakov

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

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

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

Ну а теперь к примерам. Чтобы сделать в Xunit тест параметризованным, вместо атрибута Fact метод нужно пометить атрибутом Theory и указать источник данных.

Самый простой способ с помощью InlineDataAttribute:[Theory]
[
InlineData('s1', 'S1', true)]
[
InlineData('s1', 'S2', false)]
public void TestCaseInsensitiveEquality(string s1, string s2, bool
areEqual)
{
if (areEqual)
Assert.Equal(s1, s2, ignoreCase: true),
else
Assert.NotEqual(s1, s2, new CaseInsensitiveStringComparer()),
}

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

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

Тут есть несколько вариантов: можно использовать класс-генератор или фабричный метод. В первом случае нужно использовать ClassDataAttribute с помощью которого нужно указать класс, реализующий IEnumerable<,object[]>,, или же воспользоваться MemberDataAttribute, которому можно передать имя статическокго метода, возвращающего IEnumerable<,object[]>,.[Theory]
[
MemberData(nameof(GetEqualityTestCases))]
public void TestEquality(string s1, string s2, bool
areEqual)
{
if (areEqual)
Assert.Equal(s1, s2, ignoreCase: true),
else
Assert.NotEqual(s1, s2, new CaseInsensitiveStringComparer
()),
}
private static IEnumerable<,object
[]>, GetEqualityTestCases()
{
yield return new object[] { '21', 's1', true },
}

(Я привожу пример лишь для MemberDataAttribute, поскольку нахожу этот вариант более простым и понятным. Как-то я не вижу смысла использовать ClassDataAttribute, тем более что в случае MemberDataAttribute всегда можно использовать свойство MemberType и вынести фабричный метод за пределы тестового класса.)

Очень важно, чтобы фабричный метод был статическим. Входные данные параметризованных тестов являются статической информацией и не должны основываться на состоянии тестируемого кода (что было бы возможно, если бы фабричный метод мог бы быть экземплярным). Поэтому, при попытке сделать метод экземплярным вы получите ошибку: Could not find public static member (property, field, or method) named 'GetEqualityTestCases' on Xunit4Fun.Samples.MemberDataSample. (Любопытно, что сообщение об ошибке намекает, что метод должен быть открытым, хотя, на самом деле, это не так).

Если все фабричные методы вменяемы и сконфигурированы правильно, то при наличии xunit.runner.visualstudio, вы должны увидеть параметризованные тесты в окне Test Explorer:

clip_image002

TheoryData и TheoryData<,T>,

Помимо использование методов, в атрибуте MemberDataAttribute можно указать и свойство, которое должно возвращать список object[], каждый элемент которого будет соответствовать аргументам метода.

Но у подхода с массивом объектов есть несколько неприятных моментов. Во-первых, этот подход совсем не строготипизированный, что позволяет вернуть разнородные данные и отгрести во время исполнения. Во-вторых, использование массива объектов для метода с одним параметром выглядит довольно некрасиво:[Theory]
[
MemberData(nameof(ParseTestCases))]
public void ParseShouldBeSuccessful(string
input)
{
int dummy,
bool result = int.TryParse(input, out dummy),
Assert.True(result),
}
private static IEnumerable<,object
[]>, ParseTestCases()
{
yield return new object[] { '42' },
yield return new object[] { '-1' },
}

В этом случае, вместо массива объектов можно использовать легковесную оболочку для входных данных параметризованного теста класс TheoryData<,T>, (там их целое семейство, начиная от необобщенного класса TheoryData, заканчивая классом TheoryData<,T1, , T5>,):[Theory]
[
MemberData(nameof(ParseInput))]
public void Parse(string s, bool
shouldSucceed)
{
int dummy,
bool result = int.TryParse(s, out dummy),
Assert.Equal(shouldSucceed, result),
}
private static TheoryData<,string, bool
>, ParseInput()
{
return new TheoryData<,string, bool>,()
{
// Можно использовать синтаксис инициализации коллекций!
{ '42', true
},
{
'42c', false },
},
}

В этом случае, TheoryData<,string, bool>, является контейнером входных данных, который можно создавать с помощью удобного синтаксиса инициализации коллекций.

Заключение

Параметризованные тесты в Xunit покрывают ключевые сценарии и вполне себе удобны, хотя NUnit и несколько более функционален в этом плане.

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

Дополнительные ссылки

  • Параметризованные юнит-тесты
  • Что мне не нравится в Xunit

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).