Разделы портала

Онлайн-тренинги

.
Тестирование, управляемое через данные, на C# с NUnit и RestSharp
16.04.2020 00:00

Автор: Баз Джикстра (Bas Dijkstra)
Оригинал статьи
Перевод: Ольга Алифанова

Ранее я приводил примеры базовых тестов на C# для REST API с использованием NUnit и библиотеки RestSharp. В этой статье я хочу поговорить об этом подробнее, показав, как сделать эти тесты управляемыми через данные.

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

Это особенно полезно при тестировании REST API, так как в их основе лежит отправка и получение данных, а также открытие бизнес-логики другим уровням архитектуры приложения (например, графическому интерфейсу пользователя) или другим приложениям (пользователям API).

Для начала рассмотрите эти тесты, написанные на RestSharp и NUnit:

[TestFixture]
public class NonDataDrivenTests
{
private const string BASE_URL = "http://api.zippopotam.us";
[Test]
public void RetrieveDataForUs90210_ShouldYieldBeverlyHills()
{
// arrange
RestClient client = new RestClient(BASE_URL);
RestRequest request =
new RestRequest("us/90210", Method.GET);
// act
IRestResponse response = client.Execute(request);
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
// assert
Assert.That(
locationResponse.Places[0].PlaceName,
Is.EqualTo("Beverly Hills")
);
}
[Test]
public void RetrieveDataForUs12345_ShouldYieldSchenectady()
{
// arrange
RestClient client = new RestClient(BASE_URL);
RestRequest request =
new RestRequest("us/12345", Method.GET);
// act
IRestResponse response = client.Execute(request);
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
// assert
Assert.That(
locationResponse.Places[0].PlaceName,
Is.EqualTo("Schenectady")
);
}
[Test]
public void RetrieveDataForCaY1A_ShouldYieldWhiteHorse()
{
// arrange
RestClient client = new RestClient(BASE_URL);
RestRequest request =
new RestRequest("ca/Y1A", Method.GET);
// act
IRestResponse response = client.Execute(request);
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
// assert
Assert.That(
locationResponse.Places[0].PlaceName,
Is.EqualTo("Whitehorse")
);
}
}

Отметьте, что тип LocationResponse – кастомный тип, который я самостоятельно определил. См. репозиторий GitHub для его внедрения.

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

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

Использование атрибута [TestCase]

Первый способ создания тестов, управляемых через данные – это использование атрибута [TestCase], предоставляемого NUnit. Вы можете добавлять несколько атрибутов [TestCase] к одному и тому же тест-методу, и уточнить комбинации входных и ожидаемых выходных параметров, которыми метод должен пользоваться.

Плюс к этому вы можете уточнять другие характеристики для индивидуальных тест-кейсов. Одна из наиболее полезных – это свойство TestName, которое можно использовать для читабельного и полезного имени конкретного кейса. Это имя также отражается в отчете, поэтому я очень рекомендую вам задавать его.

Вот как будет выглядеть наш код после рефакторинга с использованием атрибута [TestCase]:

[TestFixture]
public class DataDrivenUsingAttributesTests
{
private const string BASE_URL = "http://api.zippopotam.us";
[TestCase("us", "90210", "Beverly Hills", TestName = "Check that US zipcode 90210 yields Beverly Hills")]
[TestCase("us", "12345", "Schenectady", TestName = "Check that US zipcode 12345 yields Schenectady")]
[TestCase("ca", "Y1A", "Whitehorse", TestName = "Check that CA zipcode Y1A yields Whitehorse")]
public void RetrieveDataFor_ShouldYield
(string countryCode, string zipCode, string expectedPlaceName)
{
// arrange
RestClient client = new RestClient(BASE_URL);
RestRequest request =
new RestRequest($"{countryCode}/{zipCode}", Method.GET);
// act
IRestResponse response = client.Execute(request);
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
// assert
Assert.That(
locationResponse.Places[0].PlaceName,
Is.EqualTo(expectedPlaceName)
);
}
}

Намного лучше! Теперь нам нужно определить логику теста только один раз, и NUnit сам проведет итерации согласно значениям, определенным в атрибутах [TestCase]:

Однако у этого подхода есть несколько недостатков:

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

И тут в игру вступает атрибут [TestCaseSource].

Использование атрибута [TestCaseSource]

Если вам желательно или необходимо работать с большим количеством комбинаций тестовых данных, и/или вы хотите иметь возможность задавать тестовые данные вне тест-класса, использование [TestCaseSource] может быть полезной опцией.

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

Вот пример применения [TestCaseSource] к нашим тестам:

[TestFixture]
public class DataDrivenUsingTestCaseSourceTests
{
private const string BASE_URL = "http://api.zippopotam.us";
[Test, TestCaseSource("LocationTestData")]
public void RetrieveDataFor_ShouldYield
(string countryCode, string zipCode, string expectedPlaceName)
{
// arrange
RestClient client = new RestClient(BASE_URL);
RestRequest request =
new RestRequest($"{countryCode}/{zipCode}", Method.GET);
// act
IRestResponse response = client.Execute(request);
LocationResponse locationResponse =
new JsonDeserializer().
Deserialize<LocationResponse>(response);
// assert
Assert.That(
locationResponse.Places[0].PlaceName,
Is.EqualTo(expectedPlaceName)
);
}
private static IEnumerable<TestCaseData> LocationTestData()
{
yield return new TestCaseData("us", "90210", "Beverly Hills").
SetName("Check that US zipcode 90210 yields Beverly Hills");
yield return new TestCaseData("us", "12345", "Schenectady").
SetName("Check that US zipcode 12345 yields Schenectady");
yield return new TestCaseData("ca", "Y1A", "Whitehorse").
SetName("Check that CA zipcode Y1A yields Whitehorse");
}
}

В этом примере мы определяем наши тестовые данные в отдельном методе LocationTestData(), а затем говорим тестовому методу использовать этот метод в качестве источника тесовых данных, используя атрибут [TestDataSource], который берет в качестве аргумента название метода тестовых данных.

Для ясности, тестовые данные все еще жестко закодированы в теле метода LocationTestData(), но это необязательно. Вы можете с той же легкостью написать метод, считывающий тестовые данные из любого внешнего источника – лишь бы он оставался статичным и возвращал объект типа IEnumerable, или любой объект, использующий этот интерфейс.

К тому же благодаря тому, что атрибуты [TestCase] и [TestCaseSource] принадлежат NUnit, а не RestSharp, вы можете применять принципы, проиллюстрированные в статье, и к другим типам тестов.

Однако будьте осторожны перед использованием их для тестирования через пользовательский интерфейс при помощи инструментов вроде Selenium WebDriver. С шансами вы попадете в классическую ловушку "только то, что мы это можем, не значит, что мы должны". Я считаю, что тестирование, управляемое через данные, при помощи Selenium WebDriver – это код с душком: если вы множество раз проходите через одинаковую последовательность экранов, и вся разница в них в тестовых данных, то с высокой вероятностью есть более эффективный способ тестирования той же самой бизнес-логики (к примеру, через API).

Крис МакМахон объясняет это куда изящнее в своей статье. Крайне рекомендуется к прочтению.

Для других типов тестирования (API или юнит) тестирование на основе данных может быть отличным способом улучшить поддерживаемость кода ваших тестов, и сделать его более мощным.

Все примеры кода из этой статьи можно найти на странице GitHub.

Обсудить в форуме