Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Toto téma popisuje některé specifické techniky pro kontrolery testování jednotek ve webovém rozhraní API 2. Než si přečtete toto téma, možná si budete chtít přečíst kurz testování částí ASP.NET webovém rozhraní API 2, který ukazuje, jak do řešení přidat projekt testování jednotek.
Verze softwaru používané v tomto kurzu
- Visual Studio 2017
- Webové rozhraní API 2
- Moq 4.5.30
Poznámka:
Použil jsem Moq, ale stejný princip platí pro jakýkoli mockovací framework. 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 jednotkových testech je "arrange-act-assert":
- Uspořádání: Nastavte všechny požadavky pro spuštění testu.
- Akt: Proveďte test.
- Assert: Ověřte, že test proběhl úspěšně.
Ve fázi uspořádání často použijete mock nebo stub objekty. 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 řadičích webového rozhraní API:
- Akce vrátí správný typ odpovědi.
- Neplatné parametry vrací správnou chybovou odpověď.
- Akce volá správnou metodu ve vrstvě úložiště nebo služby.
- Pokud odpověď obsahuje doménový model, ověřte typ modelu.
Toto jsou některé obecné věci, které je potřeba testovat, ale specifika závisí na implementaci kontroleru. Konkrétně se jedná o velký rozdíl v tom, jestli akce kontroleru vrací 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é vracejí 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á injektování závislostí k vložení IProductRepository. Díky tomu je kontroler testovatelný, protože můžete vložit napodobené úložiště. Následující jednotkový test ověří, že metoda Get zapíše Product do těla odpovědi. Předpokládejme, že repository je to 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);
}
Je důležité nastavit požadavek a konfiguraci na kontroleru. Jinak test selže s argumentem 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 jednotkovém testu.
[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ěrovací data, takže test musí tyto hodnoty nastavit. Další možností je napodobení nebo zástupný urlHelper. Tímto přístupem nahradíte výchozí hodnotu ApiController.Url napodobenou nebo zástupným kódem, která vrátí pevnou hodnotu.
Pojďme test přepsat 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á směrovací data, protože napodobení UrlHelper vrátí konstantní řetězec.
Testování akcí, které vracejí IHttpActionResult
Ve webovém rozhraní API 2 může akce kontroleru vrátit IHttpActionResult, což je analogické k ActionResult v ASP.NET MVC. Rozhraní IHttpActionResult definuje vzor příkazu pro vytváření odpovědí HTTP. Místo přímého vytvoření odpovědi vrátí kontroler IHttpActionResult. Později kanál vyvolá IHttpActionResult a vytvoří odpověď. Tento přístup usnadňuje psaní jednotkových testů, protože můžete přeskočit spoustu nutných 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 pomocí IHttpActionResult. Pojďme se podívat, jak provádět jednotkové testy.
Akce vrátí hodnotu 200 (OK) s textem odpovědi.
Metoda Get volá Ok(product) , pokud je produkt nalezen. Při jednotkovém testu 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 jednotkový test neprovede výsledek akce. Můžete předpokládat, že výsledek akce správně vytvoří odpověď HTTP. (Proto má architektura webového rozhraní API vlastní jednotkové testy.)
Akce vrátí hodnotu 404 (Nenalezeno)
Metoda Get volá NotFound() , pokud produkt nebyl nalezen. V tomto případě jednotkový test pouze zkontroluje, jestli je návratový typ 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.
Metoda Delete volá Ok() pro vrácení prázdné odpovědi HTTP 200. Podobně jako v předchozím příkladu jednotkový test zkontroluje 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) se záhlavím Umístění.
Metoda Post zavolá CreatedAtRoute a vrátí odpověď HTTP 201 s URI v hlavičce Location. V jednotkovém testu ověřte, že akce nastaví správné směrovací hodnoty.
[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ší hodnotu 2xx s textem odpovědi.
Metoda Put volá Content k vrácení odpovědi HTTP 202 (Accepted) s textem odpovědi. Tento případ je podobný návratu stavu 200 (OK), ale jednotkový test by měl také zkontrolovat 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);
}