Testování jednotek kontrolerů webového rozhraní API 2 technologie ASP.NET
Toto téma popisuje některé konkrétní techniky pro kontrolery testování jednotek ve webovém rozhraní API 2. Před čtením tohoto tématu si možná budete chtít přečíst kurz Testování jednotek ASP.NET webovém rozhraní API 2, který ukazuje, jak do řešení přidat projekt testování jednotek.
Verze softwaru použité v kurzu
- Visual Studio 2017
- Webové rozhraní API 2
- Moq 4.5.30
Poznámka
Použil jsem Moq, ale stejná myšlenka platí pro všechny napodobování architektury. Moq 4.5.30 (a novější) podporuje Visual Studio 2017, Roslyn a .NET 4.5 a novější verze.
Běžným vzorem v testech jednotek je "arrange-act-assert":
- Uspořádat: Nastavte všechny požadavky, aby se test spustil.
- Akce: Proveďte test.
- Kontrolní výraz: Ověřte, jestli byl test úspěšný.
V kroku uspořádání budete často používat objekty napodobení nebo zástupných procedur. Tím se minimalizuje počet závislostí, takže test se zaměřuje na testování jedné věci.
Tady je několik věcí, které byste měli testovat v kontrolérech webového rozhraní API:
- Akce vrátí správný typ odpovědi.
- Neplatné parametry vrátí správnou chybovou odpověď.
- Akce volá správnou metodu v úložišti nebo vrstvě služby.
- Pokud odpověď obsahuje doménový model, ověřte typ modelu.
Toto jsou některé obecné věci, které je potřeba otestovat, ale specifika závisí na implementaci kontroleru. Konkrétně je velmi důležité, jestli akce kontroleru vrátí HttpResponseMessage nebo IHttpActionResult. Další informace o těchto typech výsledků najdete v tématu Výsledky akcí ve webovém rozhraní API 2.
Testování akcí, které vrací zprávu HttpResponseMessage
Tady je příklad kontroleru, jehož akce vrací 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;
}
}
Všimněte si, že kontroler používá injektáž závislostí k vložení .IProductRepository
Díky tomu bude kontroler lépe testovatelný, protože můžete vložit napodobené úložiště. Následující test jednotek ověří, že Get
metoda zapíše Product
do těla odpovědi . Předpokládejme, že repository
je napodobení 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);
}
Na kontroleru je důležité nastavit požadavek a konfiguraci . V opačném případě test selže s argumentNullException nebo InvalidOperationException.
Testování generování odkazů
Metoda Post
volá UrlHelper.Link k vytvoření odkazů v odpovědi. To vyžaduje trochu více nastavení v testu jednotek:
[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);
}
Třída UrlHelper potřebuje adresu URL požadavku a směrovat data, takže test musí pro tyto hodnoty nastavit hodnoty. Další možností je napodobení nebo zástupný urlhelper. S tímto přístupem nahradíte výchozí hodnotu ApiController.Url napodobenou nebo zástupným kódem, který vrací pevnou hodnotu.
Pojďme přepsat test pomocí architektury Moq . Moq
Nainstalujte balíček NuGet do testovacího projektu.
[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);
}
V této verzi nemusíte nastavovat žádná data tras, protože napodobená adresa UrlHelper vrací konstantní řetězec.
Testování akcí, které vrací IHttpActionResult
Ve webovém rozhraní API 2 může akce kontroleru vrátit IHttpActionResult, což je obdobou actionResult v ASP.NET MVC. Rozhraní IHttpActionResult definuje příkazový vzor pro vytváření odpovědí HTTP. Místo vytvoření odpovědi přímo vrátí kontroler hodnotu IHttpActionResult. Později kanál vyvolá IHttpActionResult a vytvoří odpověď. Tento přístup usnadňuje psaní testů jednotek, protože můžete přeskočit spoustu nastavení, které je potřeba pro HttpResponseMessage.
Tady je příklad kontroleru, jehož akce vrací 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);
}
}
Tento příklad ukazuje některé běžné vzory při použití funkce IHttpActionResult. Pojďme se podívat, jak je otestovat.
Akce vrátí hodnotu 200 (OK) s textem odpovědi.
Metoda Get
volá Ok(product)
, pokud je produkt nalezen. V testu jednotek se ujistěte, že návratový typ je OkNegotiatedContentResult a vrácený produkt má správné ID.
[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);
}
Všimněte si, že test jednotek nespustí výsledek akce. Můžete předpokládat, že výsledek akce vytvoří odpověď HTTP správně. (To je důvod, proč má architektura webového rozhraní API vlastní testy jednotek!)
Akce vrátí 404 (nenalezena)
Metoda Get
volá NotFound()
, pokud nebyl nalezen produkt. V tomto případě test jednotek pouze zkontroluje, jestli návratový typ je 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));
}
Akce vrátí hodnotu 200 (OK) bez textu odpovědi.
Volání Delete
metody Ok()
vrátí prázdnou odpověď HTTP 200. Stejně jako v předchozím příkladu test jednotek kontroluje návratový typ, v tomto případě 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));
}
Akce vrátí hodnotu 201 (Vytvořeno) s hlavičkou Location (Umístění).
Volání Post
metody CreatedAtRoute
vrátí odpověď HTTP 201 s identifikátorem URI v hlavičce Location. V testu jednotek ověřte, že akce nastaví správné hodnoty směrování.
[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"]);
}
Akce vrátí další 2xx s textem odpovědi.
Metoda Put
volá Content
, aby vrátila odpověď HTTP 202 (přijato) s textem odpovědi. Tento případ se podobá vrácení chyby 200 (OK), ale test jednotek by měl zkontrolovat také stavový kód.
[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);
}