Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этом разделе описываются некоторые конкретные методы для контроллеров модульного тестирования в веб-API 2. Перед прочтением этого раздела может потребоваться ознакомиться с руководством модульного тестирования веб-API ASP.NET 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":
- Упорядочение: настройте все необходимые условия для запуска теста.
- Действие. Выполните тест.
- 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);
}