Интеграционное тестирование

Steve Smith

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

В этой статье:

Скачайте пример с GitHub.

Введение в интеграционное тестирование

Интеграционные тесты проверяют, чтобы различные части приложения корректно работали вместе. В отличии от ``модульного тестирования`_ интеграционные тесты часто запрашивают компоненты инфраструктуры приложения, например, БД, файловую систему, веб запросы и ответы и так далее. На месте этих компонентов юнит тесты используют mock-объекты, а интеграционные тесты должны проверять, хорошо ли эти компоненты работают в системе.

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

Совет

Если одно и то же поведение можно протестировать с помощью юнит теста и интеграционного теста, выберите юнит тест, поскольку он фактически всегда будет быстрее. У вас могут быть сотни юнит тестов, но всего лишь несколько интеграционных тестов, покрывающих наиболее важные сценарии.

Тестирование логики внутри методов обычно является задачей юнит тестов. А интеграционные тесты входят в игру тогда, когда надо проверить, как приложение работает во фреймворке (например, ASP.NET Core) или с БД. Вам не нужно создавать слишком много интеграционных тестов, чтобы проверить, что вы можете добавить запись или извлечь ее из БД. Вам не нужно тестировать каждое возможное изменение в коде для доступа к данным - вы просто должны убедиться, что приложение работает должным образом.

Интеграционное тестирование в ASP.NET Core

Чтобы настроить интеграционные тесты, вам надо создать тестовый проект, связанный с вашим ASP.NET Core веб проектом, и установить механизм запуска тестов. Этот процесс описан в документации Unit testing, наряду с более подробными инструкциями по запуску тестов и рекомендациями по именованию тестов и тестовых классов.

Совет

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

Тестовый хост

ASP.NET Core включает тестовый хост, который может быть добавлен в проекты интеграционных тестов и используется для хостинга ASP.NET Core приложений, обрабатывая тестовые запросы без необходимости реального веб хостинга. В данный пример мы включили проект интеграционных тестов, который будет использовать `xUnit`_ и тестовый хост, как вы видите ниже:

"dependencies": {
  "PrimeWeb": "1.0.0",
  "xunit": "2.1.0",
  "dotnet-test-xunit": "1.0.0-rc2-build10025",
  "Microsoft.AspNetCore.TestHost": "1.0.0"
},

После того как пакет Microsoft.AspNet.TestHost будет включен в проект, вы сможете создать и настроить TestServer. Следующий тест показывает, как проверить, то запрос, отправленный к корневой директории сайта, возвращает “Hello World!”, и этот тест должен пройти успешно для шаблона по умолчанию ASP.NET Core Empty.

private readonly TestServer _server;
private readonly HttpClient _client;
public PrimeWebDefaultRequestShould()
{
    // Arrange
    _server = new TestServer(new WebHostBuilder()
        .UseStartup<Startup>());
    _client = _server.CreateClient();
}

[Fact]
public async Task ReturnHelloWorld()
{
    // Act
    var response = await _client.GetAsync("/");
    response.EnsureSuccessStatusCode();

    var responseString = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Equal("Hello World!",
        responseString);
}

Эти тесты используют паттерн Arrange-Act-Assert, но в данном случае этап Arrange проходит в конструкторе, который создает экземпляр TestServer. Здесь будет использоваться WebHostBuilder для создания TestHost; в данном случае мы передаем метод Configure из класса SUT Startup. Этот метод используется для настройки потока запросов TestServer идентично тому, как будет настроен SUT сервер.

В части Act теста к экземпляру TestServer делается запрос для пути “/”, а ответ считывается в строку. Эта строка затем сравнивается с ожидаемой строкой “Hello World!”. Если они одинаковы, тест пройдет успешно, иначе - нет.

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

public class PrimeWebCheckPrimeShould
{
    private readonly TestServer _server;
    private readonly HttpClient _client;
    public PrimeWebCheckPrimeShould()
    {
        // Arrange
        _server = new TestServer(new WebHostBuilder()
            .UseStartup<Startup>());
        _client = _server.CreateClient();
    }

    private async Task<string> GetCheckPrimeResponseString(
        string querystring = "")
    {
        var request = "/checkprime";
        if(!string.IsNullOrEmpty(querystring))
        {
            request += "?" + querystring;
        }
        var response = await _client.GetAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }

    [Fact]
    public async Task ReturnInstructionsGivenEmptyQueryString()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString();

        // Assert
        Assert.Equal("Pass in a number to check in the form /checkprime?5",
            responseString);
    }
    [Fact]
    public async Task ReturnPrimeGiven5()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString("5");

        // Assert
        Assert.Equal("5 is prime!",
            responseString);
    }

    [Fact]
    public async Task ReturnNotPrimeGiven6()
    {
        // Act
        var responseString = await GetCheckPrimeResponseString("6");

        // Assert
        Assert.Equal("6 is NOT prime!",
            responseString);
    }
}

Обратите внимание, что мы не пытаемся протестировать правильность нашего механизма первичной проверки, нам просто нужно увидеть, что веб приложение делает то, чего мы ожидаем. У нас уже есть юнит тесты, которые работают с PrimeService, как вы видите:

../_images/test-explorer.png

Примечание

О юнит тестах вы можете узнать больше в статье Unit testing.

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

Рефакторинг для использования связующего ПО

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

  public void Configure(IApplicationBuilder app,
      IHostingEnvironment env)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();
      }

      app.Run(async (context) =>
      {
          if (context.Request.Path.Value.Contains("checkprime"))
          {
              int numberToCheck;
              try
              {
                  numberToCheck = int.Parse(context.Request.QueryString.Value.Replace("?",""));
                  var primeService = new PrimeService();
                  if (primeService.IsPrime(numberToCheck))
                  {
                      await context.Response.WriteAsync(numberToCheck + " is prime!");
                  }
                  else
                  {
                      await context.Response.WriteAsync(numberToCheck + " is NOT prime!");
                  }
              }
              catch
              {
                  await context.Response.WriteAsync("Pass in a number to check in the form /checkprime?5");
              }
          }
          else
          {
              await context.Response.WriteAsync("Hello World!");
          }
      });
  }

Этот код работает, но он далек от того, как бы мы хотели реализовать такой функционал в ASP.NET Core приложении, даже таком простом, как наше. Представьте себе, на что будет похож метод Configure, если нам понадобится записывать в него больше кода каждый раз, когда мы будем добавлять другую конечную точку для URL!

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

Кроме того, мы можем использовать ASP.NET Связующее ПО (Middleware), и тогда мы сможем инкапсулировать логику первичной проверки в отдельном классе и достичь лучшего разделения ответственности внутри метода Configure.

Мы хотим, чтобы путь к связующему ПО был указан как параметр, так что класс ожидает в своем конструкторе RequestDelegate и PrimeCheckerOptions. Если путь запроса не соответствует тому, который ожидает увидеть связующее ПО, мы просто вызываем следующее связующее ПО в цепочке. Оставшаяся часть кода, который был в Configure, теперь находится в Invoke.

Примечание

Поскольку наше связующее ПО зависит от сервиса PrimeService, мы также запрашиваем через конструктор экземпляр этого сервиса. Фреймворк предоставит этот сервис через Внедрение зависимостей (Dependency Injection), и мы предполагаем, что он настроен (например, в ConfigureServices).

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using PrimeWeb.Services;
using System;
using System.Threading.Tasks;

namespace PrimeWeb.Middleware
{
    public class PrimeCheckerMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly PrimeCheckerOptions _options;
        private readonly PrimeService _primeService;

        public PrimeCheckerMiddleware(RequestDelegate next,
            PrimeCheckerOptions options,
            PrimeService primeService)
        {
            if (next == null)
            {
                throw new ArgumentNullException(nameof(next));
            }
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }
            if (primeService == null)
            {
                throw new ArgumentNullException(nameof(primeService));
            }

            _next = next;
            _options = options;
            _primeService = primeService;
        }

        public async Task Invoke(HttpContext context)
        {
            var request = context.Request;
            if (!request.Path.HasValue ||
                request.Path != _options.Path)
            {
                await _next.Invoke(context);
            }
            else
            {
                int numberToCheck;
                if (int.TryParse(request.QueryString.Value.Replace("?", ""), out numberToCheck))
                {
                    if (_primeService.IsPrime(numberToCheck))
                    {
                        await context.Response.WriteAsync($"{numberToCheck} is prime!");
                    }
                    else
                    {
                        await context.Response.WriteAsync($"{numberToCheck} is NOT prime!");
                    }
                }
                else
                {
                    await context.Response.WriteAsync($"Pass in a number to check in the form {_options.Path}?5");
                }
            }
        }
    }
}

Примечание

Поскольку связующее ПО работает как конечная точка в цепочке делегатов запроса, если путь соответствует, то _next.Invoke не будет вызван в том случае, если это связующее ПО обрабатывает запрос.

Когда связующее По на месте и созданы некоторые полезные методы расширения для облегчения конфигурации, после рефакторинга метод Configure выглядит вот так:

public void Configure(IApplicationBuilder app,
    IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UsePrimeChecker();

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

После рефакторинга мы также знаем, что приложение работает, как и раньше, поскольку интеграционные тесты прошли успешно.

Совет

Также вам неплохо коммитить изменения после того, как вы провели рефакторинг и все тесты прошли успешно. Если вы практикуете Test Driven Development, добавьте коммит в цикл Red-Green-Refactor.

Резюме

Интеграционное тестирование предоставляет более высокий уровень верификации, нежели модульное тестирование. При интеграционном тестировании мы проверяем инфраструктуру и то, как различные части приложения работают совместно. ASP.NET Core предлагает TestServer, который значительно упрощает запуск интеграционных тестов.

Дополнительные ресурсы

Поделись хорошей новостью с друзьями!
Следи за новостями!