Egységtesztelési vezérlők a ASP.NET Web API 2-ben

Ez a témakör a Web API 2 egységtesztelési vezérlőinek néhány konkrét technikáit ismerteti. A témakör elolvasása előtt érdemes lehet elolvasni a Unit Testing ASP.NET Web API 2 oktatóanyagot, amely bemutatja, hogyan adhat hozzá egységtesztelési projektet a megoldáshoz.

Az oktatóanyagban használt szoftververziók

Megjegyzés:

Moq-t használtam, de ugyanez az elképzelés vonatkozik minden szimulálási keretrendszerre. A Moq 4.5.30 (és újabb verziók) támogatják a Visual Studio 2017, Roslyn és .NET 4.5 és újabb verzióit.

Az egységtesztek gyakori mintája az "arrange-act-assert":

  • Elrendezés: A teszt futtatásának előfeltételeinek beállítása.
  • Művelet: Végezze el a tesztet.
  • Állítás: Ellenőrizze, hogy a teszt sikeres volt-e.

Az elrendezési lépésben gyakran használunk makett- vagy csonkobjektumokat. Ez minimalizálja a függőségek számát, ezért a teszt egy dolog tesztelésére összpontosít.

Íme néhány dolog, amelyet a webes API-vezérlőkben érdemes egységben tesztelni:

  • A művelet a megfelelő választípust adja vissza.
  • Az érvénytelen paraméterek a helyes hibaválaszt adják vissza.
  • A művelet a megfelelő metódust hívja meg az adattárban vagy a szolgáltatási rétegben.
  • Ha a válasz tartománymodellt tartalmaz, ellenőrizze a modell típusát.

Ezek az általános tesztelendő dolgok, de a konkrétumok a vezérlő implementációjától függenek. Különösen nagy különbség, hogy a vezérlőműveletek httpResponseMessage vagy IHttpActionResult értéket adnak vissza. További információ ezekről az eredménytípusokról: Műveleteredmények a Web Api 2-ben.

HttpResponseMessage-t visszaadó tesztelési műveletek

Íme egy példa egy vezérlőre, amelynek műveletei HttpResponseMessage értéket adnak vissza.

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;
    }
}

Figyelje meg, hogy a vezérlő a függőség-injektálást használja egy IProductRepository beszúrására. Ez tesztelhetőbbé teszi a vezérlőt, mert beilleszthet egy mock objektumot. Az alábbi egységteszt ellenőrzi, hogy a Get metódus ír-e a Product választörzsbe. Tegyük fel, hogy repository ez egy mock 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);
}

Fontos a kérés és a konfiguráció beállítása a vezérlőn. Ellenkező esetben a teszt egy ArgumentNullException vagy InvalidOperationException hibával fog meghiúsulni.

A Post metódus meghívja UrlHelper.Link , hogy hivatkozásokat hozzon létre a válaszban. Ehhez egy kicsit több beállításra van szükség az egységtesztben:

[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);
}

Az UrlHelper osztálynak szüksége van a kérelem URL-címére és az útvonaladatokra, ezért a tesztnek meg kell határoznia ezek értékeit. Egy másik lehetőség a "mock" vagy "stub" technikák alkalmazása az UrlHelper esetében. Ezzel a módszerrel az ApiController.URL alapértelmezett értékét egy rögzített értéket visszaadó modellre vagy csonkverzióra cseréli.

Írjuk át a tesztet a Moq-keretrendszer használatával. Telepítse a Moq NuGet-csomagot a tesztprojektben.

[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);
}

Ebben a verzióban nem kell útvonaladatokat beállítania, mert az URLHelper minta egy állandó sztringet ad vissza.

Az IHttpActionResult értéket visszaadó tesztelési műveletek

A Web API 2-ben a vezérlőművelet IHttpActionResult értéket ad vissza, amely hasonló az ASP.NET MVC ActionResult-hoz . Az IHttpActionResult felület parancsmintát határoz meg a HTTP-válaszok létrehozásához. A válasz közvetlen létrehozása helyett a vezérlő egy IHttpActionResult értéket ad vissza. Később a folyamat meghívja az IHttpActionResult parancsot a válasz létrehozásához. Ez a megközelítés megkönnyíti az egységtesztek írását, mivel a HttpResponseMessage-hez szükséges beállítások nagy részét kihagyhatja.

Íme egy példavezérlő, amelynek műveletei IHttpActionResult értéket adnak vissza.

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);
    }    
}

Ez a példa néhány gyakori mintát mutat be az IHttpActionResult használatával. Nézzük meg, hogyan teszteljük őket egységben.

A művelet 200 (OK) értéket ad vissza válasz törzzsel

A Get metódus meghívja Ok(product) , ha a termék megtalálható. Az egységtesztben győződjön meg arról, hogy a visszatérési típus OkNegotiatedContentResult, a visszaadott termék pedig a megfelelő azonosítóval rendelkezik.

[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);
}

Figyelje meg, hogy az egységteszt nem hajtja végre a művelet eredményét. Feltételezheti, hogy a művelet eredménye helyesen hozza létre a HTTP-választ. (Ezért a Web API-keretrendszer saját egységtesztekkel rendelkezik!)

A művelet a 404-et adja vissza (nem található)

A Get metódus meghívja NotFound() , ha a termék nem található. Ebben az esetben az egységteszt csak ellenőrzi, hogy a visszatérési típus NotFoundResult-e.

[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));
}

A művelet 200 (OK) kódot ad vissza válasz törzs nélkül.

A Delete metódus meghívja a Ok()-et, hogy visszaadjon egy üres HTTP 200 választ. Az előző példához hasonlóan az egységteszt ellenőrzi a visszatérési típust, ebben az esetben az OkResult típust.

[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));
}

A művelet 201-at (Létrehozva) ad vissza egy Location fejléccel

A Post metódus meghívja a CreatedAtRoute-et, hogy visszaadjon egy HTTP 201-választ egy URI-val a Hely megjelölés fejlécben. Az egységtesztben ellenőrizze, hogy a művelet a megfelelő útválasztási értékeket állítja-e be.

[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"]);
}

A művelet egy másik 2xx értéket ad vissza választörzsgel

A Put metódus meghívja Content a HTTP 202 (Elfogadott) válasz visszaadására, válasz törzzsel. Ez az eset hasonlít a 200-ra (OK) való visszatéréshez, de az egységtesztnek az állapotkódot is ellenőriznie kell.

[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);
}

További források