Тестирование контроллеров WebAPI

Нужно или нет тестировать контроллеры — я не редко слышу эти споры. Но факт остается фактом — есть случаи когда приложение реализует API для взаимодействия с внешним миром и такой API должен быть задокументирован и протестирован. Поэтому несколько слов о том, как тестировать WebAPI-контроллеры.

Как известно, контроллеры WebAPI могут возвращать результат тремя различными способами:

  1. Используя объект HttpResponseMessage - в этом случае результат ответа генерируется прямо в контроллере и отдается на сторону клиента "как есть".
  2. Используя обертку IHttpActionResult - при этом в процессе обработки запроса он преобразуется в HttpResponseMessage и отправляется клиенту.
  3. Используя любой другой CLR-объект, содержащий данные - тогда объект сериализуется, упаковывается в сообщение ответа и отправляется в ответ на запрос.

Каждый из этих пунктов меняет подход к реализации контроллера, а значит и влияет на структуру тестов. Поэтому рассмотрим каждый случай отдельно.

HttpResponseMessage

Контроллер в этом случае выглядит так:

public class JoinController : ApiController
{
  public HttpResponseMessage JoinAsUser(int id)
  {
    if (id < 0)
      return Request.CreateResponse(HttpStatusCode.BadRequest);

    // Логика

    return Request.CreateResponse(HttpStatusCode.OK);
  }

В результате работы действия контроллера генерируется объект HttpResponseMessage, содержимое которого мы можем проверить в тесте.

Подготовительные действия в тесте заключаются в создании и инициализации контроллера, а также задании маршрутов:

// Arrage
var controller = new JoinController
{
  Request = new HttpRequestMessage
  {
    RequestUri = new Uri("http://localhost/api/join/joinAsUser")
  },
  Configuration = new HttpConfiguration(),
  RequestContext =
  {
    RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary
        {
            { "controller", "join" },
            { "action", "joinAsUser" }
        })
  }
};

controller.Configuration.Routes.MapHttpRoute(
  name: "DefaultApi",
  routeTemplate: "api/{controller}/{action}/{id}",
  defaults: new { id = RouteParameter.Optional });

После чего можно обратиться к действию контроллера и проверить результат:

// Act
var result = controller.JoinAsUser(5);

// Assert
Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);

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

Итоговый код теста:

[TestMethod]
public void JoinAsUserTest()
{
  // Arrage
  var controller = new JoinController
  {
    Request = new HttpRequestMessage
    {
      RequestUri = new Uri("http://localhost/api/join/joinAsUser")
  },
    Configuration = new HttpConfiguration(),
    RequestContext =
    {
      RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary
        {
            { "controller", "join" },
            { "action", "joinAsUser" }
        })
    }
  };

  controller.Configuration.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional });


  // Act
  var result = controller.JoinAsUser(5);

  // Assert
  Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);
}

IHttpActionResult

Реализация действия контроллера в этом случае может выглядеть так:

public class JoinController : ApiController
{
  public IHttpActionResult Leave(int id)
  {
    if (id < 0)
        return BadRequest();

    // Логика

    return Ok();
  }

IHttpActionResult - это обертка, которая обладает семантикой, например - запрос обработался успешно, с ошибкой, или возникли проблемы с авторизацией. В базовом классе контроллера WebAPI уже имеется набор заготовленных методов для формирования ответа в таком стиле - Ok(), Ok<>(), Redirect(), BadRequest() и т.п. Мы также можем создать свои типы ответов.

В этом случае тест будет сводиться к проверке типа ответа и его содержимого:

// Act
var result = controller.Leave(5);

// Assert
Assert.IsInstanceOfType(result, typeof(OkResult));

Как и в предыдущем случае, условия проверки могут быть более сложными.

Итоговый код теста:

[TestMethod]
public void LeaveTest()
{
  // Arrage
  var controller = new JoinController
  {
    Request = new HttpRequestMessage
    {
      RequestUri = new Uri("http://localhost/api/join/leave")
    },
    Configuration = new HttpConfiguration(),
    RequestContext =
    {
      RouteData = new HttpRouteData(
        route: new HttpRoute(),
        values: new HttpRouteValueDictionary
        {
            { "controller", "join" },
            { "action", "leave" }
        })
    }
  };

  controller.Configuration.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional });


  // Act
  var result = controller.Leave(5);

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

CLR-объект в качестве результата

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

Контроллер в этом случае выглядит так:

public class JoinController : ApiController
{
  public string CheckStatus(int id)
  {
    if (id < 0)
      throw new HttpException(400, "Bad request");

    return "Joined";
  }

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

Тестирование в данном случае наиболее прозрачное - вызывается соответствующее действие и проверяется сам объект:

// Act
var result = controller.CheckStatus(5);

// Assert
Assert.AreEqual("Joined", result);

Полный код теста:

[TestMethod]
public void CheckStatusTest()
{
  // Arrage
  var controller = new JoinController
  {
    Request = new HttpRequestMessage
    {
      RequestUri = new Uri("http://localhost/api/join/сheckStatus")
    },
    Configuration = new HttpConfiguration(),
    RequestContext =
    {
      RouteData = new HttpRouteData(
              route: new HttpRoute(),
              values: new HttpRouteValueDictionary
      {
          { "controller", "join" },
          { "action", "сheckStatus" }
      })
    }
  };

  controller.Configuration.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{action}/{id}",
    defaults: new { id = RouteParameter.Optional });


  // Act
  var result = controller.CheckStatus(5);

  // Assert
  Assert.AreEqual("Joined", result);
}