Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этом разделе описаны некоторые методы для контроллеров модульного тестирования в веб-API 2. Прежде чем читать эту статью, вам может потребоваться прочитать учебник модульного тестирования ASP.NET веб-API 2, в котором показано, как добавить проект модульного тестирования в решение.
Версии программного обеспечения, используемые в руководстве
- Visual Studio 2017
- Веб-API 2
- Moq 4.5.30
Замечание
Я использовал Moq, но та же идея применяется к любому фреймворку мокирования. Moq 4.5.30 (и более поздних версий) поддерживает Visual Studio 2017, Roslyn, а также .NET 4.5 и выше.
Распространенный шаблон в модульных тестах — "arrange-act-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. Это делает контроллер более тестируемым, так как вы можете внедрить макет репозитория. Следующий модульный тест проверяет, записывает ли метод GetProduct в тело ответа. Предположим, что 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
В Web 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 (Created) с заголовком 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);
}