Kontrolery testów jednostkowych we wzorcu ASP.NET Web API 2
W tym temacie opisano niektóre konkretne techniki kontrolerów testowania jednostkowego w internetowym interfejsie API 2. Przed przeczytaniem tego tematu warto przeczytać samouczek Testowanie jednostkowe ASP.NET internetowego interfejsu API 2, który pokazuje, jak dodać projekt testowy jednostkowy do rozwiązania.
Wersje oprogramowania używane w samouczku
- Visual Studio 2017
- Internetowy interfejs API 2
- Moq 4.5.30
Uwaga
Użyłem Moq, ale ten sam pomysł ma zastosowanie do każdej szyderczej struktury. Program Moq 4.5.30 (i nowsze) obsługuje programy Visual Studio 2017, Roslyn i .NET 4.5 i nowsze.
Typowym wzorcem testów jednostkowych jest "arrange-act-assert":
- Rozmieść: skonfiguruj wszelkie wymagania wstępne dotyczące przebiegu testu.
- Działanie: przeprowadź test.
- Potwierdzenie: sprawdź, czy test zakończył się pomyślnie.
W kroku rozmieszczania często będziesz używać makiety lub obiektów wycinkowych. To minimalizuje liczbę zależności, więc test koncentruje się na testowaniu jednej rzeczy.
Oto kilka rzeczy, które należy przetestować jednostkowym na kontrolerach internetowego interfejsu API:
- Akcja zwraca prawidłowy typ odpowiedzi.
- Nieprawidłowe parametry zwracają poprawną odpowiedź o błędzie.
- Akcja wywołuje poprawną metodę w repozytorium lub warstwie usługi.
- Jeśli odpowiedź zawiera model domeny, sprawdź typ modelu.
Są to niektóre ogólne kwestie do przetestowania, ale specyfika zależy od implementacji kontrolera. W szczególności ma to dużą różnicę, czy akcje kontrolera zwracają httpResponseMessage czy IHttpActionResult. Aby uzyskać więcej informacji na temat tych typów wyników, zobacz Wyniki akcji w internetowym interfejsie API 2.
Akcje testowania zwracające komunikat HttpResponseMessage
Oto przykład kontrolera, którego akcje zwracają komunikat 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;
}
}
Zwróć uwagę, że kontroler używa iniekcji zależności do wstrzykiwania IProductRepository
elementu . To sprawia, że kontroler jest bardziej testowalny, ponieważ można wstrzyknąć makiety repozytorium. Poniższy test jednostkowy sprawdza, czy Get
metoda zapisuje element w Product
treści odpowiedzi. Załóżmy, że repository
jest wyśmiewane 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);
}
Ważne jest ustawienie żądania i konfiguracji na kontrolerze. W przeciwnym razie test zakończy się niepowodzeniem z argumentemNullException lub InvalidOperationException.
Testowanie generowania linków
Metoda Post
wywołuje UrlHelper.Link , aby utworzyć linki w odpowiedzi. Wymaga to nieco więcej konfiguracji w teście jednostkowym:
[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);
}
Klasa UrlHelper wymaga adresu URL żądania i danych trasy, dlatego test musi ustawić wartości dla tych elementów. Inną opcją jest wyśmiewane lub wycinkowe UrlHelper. Dzięki temu podejściu zastąp wartość domyślną elementu ApiController.Url pozorną lub wersją wycinkową zwracającą stałą wartość.
Napiszmy ponownie test przy użyciu platformy Moq . Moq
Zainstaluj pakiet NuGet w projekcie testowym.
[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);
}
W tej wersji nie trzeba konfigurować żadnych danych trasy, ponieważ makieta UrlHelper zwraca stały ciąg.
Akcje testowania zwracające IHttpActionResult
W internetowym interfejsie API 2 akcja kontrolera może zwrócić wartość IHttpActionResult, która jest analogiczna do elementu ActionResult w ASP.NET MVC. Interfejs IHttpActionResult definiuje wzorzec polecenia do tworzenia odpowiedzi HTTP. Zamiast bezpośrednio tworzyć odpowiedź, kontroler zwraca obiekt IHttpActionResult. Później potok wywołuje element IHttpActionResult , aby utworzyć odpowiedź. Takie podejście ułatwia pisanie testów jednostkowych, ponieważ można pominąć wiele konfiguracji, które są potrzebne dla protokołu HttpResponseMessage.
Oto przykładowy kontroler, którego akcje zwracają wartość 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);
}
}
W tym przykładzie przedstawiono kilka typowych wzorców korzystających z funkcji IHttpActionResult. Zobaczmy, jak je przetestować.
Akcja zwraca 200 (OK) z treścią odpowiedzi
Metoda Get
wywołuje metodę Ok(product)
w przypadku znalezienia produktu. W teście jednostkowym upewnij się, że zwracany typ to OkNegotiatedContentResult , a zwrócony produkt ma prawidłowy identyfikator.
[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);
}
Zwróć uwagę, że test jednostkowy nie wykonuje wyniku akcji. Możesz założyć, że wynik akcji poprawnie tworzy odpowiedź HTTP. (Dlatego struktura internetowego interfejsu API ma własne testy jednostkowe!)
Akcja zwraca wartość 404 (nie znaleziono)
Metoda Get
wywołuje metodę NotFound()
, jeśli produkt nie zostanie znaleziony. W tym przypadku test jednostkowy sprawdza, czy zwracany typ to 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));
}
Akcja zwraca 200 (OK) bez treści odpowiedzi
Metoda Delete
wywołuje metodę Ok()
, aby zwrócić pustą odpowiedź HTTP 200. Podobnie jak w poprzednim przykładzie test jednostkowy sprawdza typ powrotu, w tym przypadku 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));
}
Akcja zwraca wartość 201 (Utworzono) z nagłówkiem Lokalizacja
Metoda Post
wywołuje metodę CreatedAtRoute
, aby zwrócić odpowiedź HTTP 201 z identyfikatorem URI w nagłówku Lokalizacja. W teście jednostkowym sprawdź, czy akcja ustawia prawidłowe wartości routingu.
[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"]);
}
Akcja zwraca kolejne 2xx z treścią odpowiedzi
Metoda Put
wywołuje Content
odpowiedź HTTP 202 (Zaakceptowana) z treścią odpowiedzi. Ten przypadek jest podobny do zwracania 200 (OK), ale test jednostkowy powinien również sprawdzić kod stanu.
[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);
}