Контроллеры модульного тестирования в ASP.NET веб-API 2

В этом разделе описываются некоторые конкретные методы для контроллеров модульного тестирования в веб-API 2. Перед прочтением этого раздела может потребоваться ознакомиться с руководством модульного тестирования веб-API ASP.NET 2, в котором показано, как добавить проект модульного теста в решение.

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

Примечание

Я использовал Moq, но та же идея относится к любой макетной платформе. Moq 4.5.30 (и более поздние версии) поддерживает Visual Studio 2017, Roslyn и .NET 4.5 и более поздних версий.

Распространенный шаблон в модульных тестах — "arrange-act-assert":

  • Упорядочение: настройте все необходимые условия для запуска теста.
  • Действие. Выполните тест.
  • Assert: убедитесь, что тест выполнен успешно.

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

Ниже приведены некоторые аспекты, которые следует выполнить модульное тестирование в контроллерах веб-API.

  • Действие возвращает правильный тип ответа.
  • Недопустимые параметры возвращают правильный ответ об ошибке.
  • Действие вызывает правильный метод на уровне репозитория или службы.
  • Если ответ содержит модель предметной области, проверьте тип модели.

Это некоторые из общих вещей для тестирования, но особенности зависят от реализации контроллера. В частности, важно, возвращают ли действия контроллера httpResponseMessage или IHttpActionResult. Дополнительные сведения об этих типах результатов см. в разделе Результаты действий в веб-API 2.

Действия тестирования, возвращающие httpResponseMessage

Ниже приведен пример контроллера, действия которого возвращают httpResponseMessage.

public class ProductsController : ApiController
{
    IProductRepository _repository;

    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }

    public HttpResponseMessage Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
        return Request.CreateResponse(product);
    }

    public HttpResponseMessage Post(Product product)
    {
        _repository.Add(product);

        var response = Request.CreateResponse(HttpStatusCode.Created, product);
        string uri = Url.Link("DefaultApi", new { id = product.Id });
        response.Headers.Location = new Uri(uri);

        return response;
    }
}

Обратите внимание, что контроллер использует внедрение зависимостей для внедрения IProductRepository. Это делает контроллер более тестируемым, так как вы можете внедрить макет репозитория. Следующий модульный тест проверяет, Get записывает ли метод в Product текст ответа . Предположим, что repository является макетом IProductRepository.

[TestMethod]
public void GetReturnsProduct()
{
    // Arrange
    var controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    // Act
    var response = controller.Get(10);

    // Assert
    Product product;
    Assert.IsTrue(response.TryGetContentValue<Product>(out product));
    Assert.AreEqual(10, product.Id);
}

Важно задать запрос и конфигурацию на контроллере. В противном случае тест завершится ошибкой с исключением ArgumentNullException или InvalidOperationException.

Метод Post вызывает UrlHelper.Link для создания ссылок в ответе. Для этого требуется немного больше настройки в модульном тесте:

[TestMethod]
public void PostSetsLocationHeader()
{
    // Arrange
    ProductsController controller = new ProductsController(repository);

    controller.Request = new HttpRequestMessage { 
        RequestUri = new Uri("http://localhost/api/products") 
    };
    controller.Configuration = new HttpConfiguration();
    controller.Configuration.Routes.MapHttpRoute(
        name: "DefaultApi", 
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional });

    controller.RequestContext.RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary { { "controller", "products" } });

    // Act
    Product product = new Product() { Id = 42, Name = "Product1" };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual("http://localhost/api/products/42", response.Headers.Location.AbsoluteUri);
}

Классу UrlHelper требуется URL-адрес запроса и данные маршрута, поэтому тест должен задать для них значения. Другой вариант — макет или заглушка UrlHelper. При таком подходе вы заменяете значение по умолчанию ApiController.Url макетом или версией заглушки, которая возвращает фиксированное значение.

Давайте перепишут тест с помощью платформы Moq . Moq Установите пакет NuGet в тестовом проекте.

[TestMethod]
public void PostSetsLocationHeader_MockVersion()
{
    // This version uses a mock UrlHelper.

    // Arrange
    ProductsController controller = new ProductsController(repository);
    controller.Request = new HttpRequestMessage();
    controller.Configuration = new HttpConfiguration();

    string locationUrl = "http://location/";

    // Create the mock and set up the Link method, which is used to create the Location header.
    // The mock version returns a fixed string.
    var mockUrlHelper = new Mock<UrlHelper>();
    mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
    controller.Url = mockUrlHelper.Object;

    // Act
    Product product = new Product() { Id = 42 };
    var response = controller.Post(product);

    // Assert
    Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}

В этой версии вам не нужно настраивать данные маршрута, так как макет UrlHelper возвращает константную строку.

Действия тестирования, возвращающие IHttpActionResult

В веб-API 2 действие контроллера может возвращать IHttpActionResult, что аналогично ActionResult в ASP.NET MVC. Интерфейс IHttpActionResult определяет шаблон команд для создания HTTP-ответов. Вместо создания ответа напрямую контроллер возвращает IHttpActionResult. Позже конвейер вызывает IHttpActionResult для создания ответа. Такой подход упрощает написание модульных тестов, так как вы можете пропустить множество настроек, необходимых для HttpResponseMessage.

Ниже приведен пример контроллера, действия которого возвращают IHttpActionResult.

public class Products2Controller : ApiController
{
    IProductRepository _repository;

    public Products2Controller(IProductRepository repository)
    {
        _repository = repository;
    }

    public IHttpActionResult Get(int id)
    {
        Product product = _repository.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }

    public IHttpActionResult Post(Product product)
    {
        _repository.Add(product);
        return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
    }

    public IHttpActionResult Delete(int id)
    {
        _repository.Delete(id);
        return Ok();
    }

    public IHttpActionResult Put(Product product)
    {
        // Do some work (not shown).
        return Content(HttpStatusCode.Accepted, product);
    }    
}

В этом примере показаны некоторые распространенные шаблоны использования IHttpActionResult. Давайте посмотрим, как выполнить модульное тестирование.

Действие возвращает значение 200 (ОК) с текстом ответа

Метод Get вызывает Ok(product) , если продукт найден. В модульном тесте убедитесь, что тип возвращаемого значения — OkNegotiatedContentResult , а возвращенный продукт имеет правильный идентификатор.

[TestMethod]
public void GetReturnsProductWithSameId()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    mockRepository.Setup(x => x.GetById(42))
        .Returns(new Product { Id = 42 });

    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(42);
    var contentResult = actionResult as OkNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(42, contentResult.Content.Id);
}

Обратите внимание, что модульный тест не выполняет результат действия. Можно предположить, что результат действия создает HTTP-ответ правильно. (Именно поэтому платформа веб-API имеет собственные модульные тесты!)

Действие возвращает значение 404 (не найдено)

Метод Get вызывает , NotFound() если продукт не найден. В этом случае модульный тест просто проверяет, имеет ли тип возвращаемого значения NotFoundResult.

[TestMethod]
public void GetReturnsNotFound()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Get(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
}

Действие возвращает значение 200 (ОК) без текста ответа

Метод Delete вызывает Ok() для возврата пустого ответа HTTP 200. Как и в предыдущем примере, модульный тест проверяет тип возвращаемого значения, в данном случае OkResult.

[TestMethod]
public void DeleteReturnsOk()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Delete(10);

    // Assert
    Assert.IsInstanceOfType(actionResult, typeof(OkResult));
}

Действие возвращает значение 201 (создано) с заголовком Location

Метод Post вызывает CreatedAtRoute для возврата ответа HTTP 201 с универсальным кодом ресурса (URI) в заголовке Location. В модульном тесте убедитесь, что действие задает правильные значения маршрутизации.

[TestMethod]
public void PostMethodSetsLocationHeader()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Post(new Product { Id = 10, Name = "Product1" });
    var createdResult = actionResult as CreatedAtRouteNegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(createdResult);
    Assert.AreEqual("DefaultApi", createdResult.RouteName);
    Assert.AreEqual(10, createdResult.RouteValues["id"]);
}

Действие возвращает еще 2xx с текстом ответа

Метод Put вызывает Content для возврата ответа HTTP 202 (принято) с текстом ответа. Этот случай аналогичен возврату 200 (ОК), но модульный тест также должен проверка код состояния.

[TestMethod]
public void PutReturnsContentResult()
{
    // Arrange
    var mockRepository = new Mock<IProductRepository>();
    var controller = new Products2Controller(mockRepository.Object);

    // Act
    IHttpActionResult actionResult = controller.Put(new Product { Id = 10, Name = "Product" });
    var contentResult = actionResult as NegotiatedContentResult<Product>;

    // Assert
    Assert.IsNotNull(contentResult);
    Assert.AreEqual(HttpStatusCode.Accepted, contentResult.StatusCode);
    Assert.IsNotNull(contentResult.Content);
    Assert.AreEqual(10, contentResult.Content.Id);
}

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