Freigeben über


Komponententestcontroller in ASP.NET Web-API 2

In diesem Thema werden einige spezifische Techniken für Komponententestcontroller in Web API 2 beschrieben. Bevor Sie dieses Thema lesen, sollten Sie das Lernprogramm "Komponententests" ASP.NET Web-API 2 lesen, das zeigt, wie Sie Ihrer Lösung ein Komponententestprojekt hinzufügen.

Im Lernprogramm verwendete Softwareversionen

Hinweis

Ich habe Moq verwendet, aber die gleiche Idee gilt für jedes simulierte Framework. Moq 4.5.30 (und höher) unterstützt Visual Studio 2017, Roslyn und .NET 4.5 und höhere Versionen.

Ein gängiges Muster bei Komponententests ist "arrange-act-assert":

  • Anordnen: Richten Sie alle Voraussetzungen ein, damit der Test ausgeführt werden kann.
  • Act: Führen Sie den Test aus.
  • Assert: Überprüfen Sie, ob der Test erfolgreich war.

Im Anordnungsschritt verwenden Sie häufig simulierte oder Stubobjekte. Dadurch wird die Anzahl der Abhängigkeiten minimiert, sodass sich der Test auf das Testen einer Sache konzentriert.

Hier sind einige Dinge, die Sie in Ihren Web-API-Controllern komponententesten sollten:

  • Die Aktion gibt den richtigen Antworttyp zurück.
  • Ungültige Parameter geben die richtige Fehlerantwort zurück.
  • Die Aktion ruft die richtige Methode auf der Repository- oder Dienstebene auf.
  • Wenn die Antwort ein Domänenmodell enthält, überprüfen Sie den Modelltyp.

Dies sind einige der allgemeinen Dinge, die Sie testen müssen, aber die Besonderheiten hängen von der Controllerimplementierung ab. Insbesondere macht es einen großen Unterschied, ob Ihre Controlleraktionen HttpResponseMessage oder IHttpActionResult zurückgeben. Weitere Informationen zu diesen Ergebnistypen finden Sie unter "Aktionsergebnisse" in Der Web-API 2.

Testen von Aktionen, die HttpResponseMessage zurückgeben

Nachfolgend sehen Sie ein Beispiel für einen Controller, dessen Aktionen HttpResponseMessage zurückgeben.

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

Beachten Sie, dass der Controller eine "Abhängigkeitsinjektion" verwendet, um ein IProductRepository zu injizieren. Dies macht den Controller testfähiger, da Sie ein pseudorepository einfügen können. Der folgende Komponententest überprüft, ob die Get Methode einen Product in den Antworttext schreibt. Nehmen Sie an, dass repository ein Mock IProductRepository ist.

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

Es ist wichtig, die Anforderung und Konfiguration auf dem Controller festzulegen. Andernfalls schlägt der Test mit einer ArgumentNullException oder InvalidOperationException fehl.

Die Post Methode ruft UrlHelper.Link auf, um Verknüpfungen in der Antwort zu erstellen. Dies erfordert etwas mehr Setup im Komponententest:

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

Die UrlHelper-Klasse benötigt die Anforderungs-URL und Routendaten, sodass der Test Werte für diese festlegen muss. Eine andere Option ist UrlHelper zu simulieren oder als Stub zu verwenden. Bei diesem Ansatz ersetzen Sie den Standardwert von "ApiController.Url" durch eine simulierte oder stub-Version, die einen festen Wert zurückgibt.

Lassen Sie uns den Test mithilfe des Moq-Frameworks neu schreiben. Installieren Sie das Moq NuGet-Paket im Testprojekt.

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

In dieser Version müssen Sie keine Routendaten einrichten, da die simulierte UrlHelper eine Konstante Zeichenfolge zurückgibt.

Testen von Aktionen, die IHttpActionResult zurückgeben

In Web-API 2 kann eine Controlleraktion IHttpActionResult zurückgeben, die mit ActionResult in ASP.NET MVC vergleichbar ist. Die IHttpActionResult-Schnittstelle definiert ein Befehlsmuster zum Erstellen von HTTP-Antworten. Anstatt die Antwort direkt zu erstellen, gibt der Controller ein IHttpActionResult zurück. Später ruft die Pipeline das IHttpActionResult auf, um die Antwort zu erstellen. Dieser Ansatz erleichtert das Schreiben von Komponententests, da Sie viele setups überspringen können, die für HttpResponseMessage erforderlich sind.

Hier ist ein Beispielcontroller, dessen Aktionen IHttpActionResult zurückgeben.

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

Dieses Beispiel zeigt einige gängige Muster mit IHttpActionResult. Sehen wir uns an, wie Sie die Komponenten testen.

Aktion gibt 200 (OK) mit einem Antworttext zurück.

Die Get Methode ruft auf Ok(product) , wenn das Produkt gefunden wird. Stellen Sie im Komponententest sicher, dass der Rückgabetyp "OkNegotiatedContentResult " lautet und das zurückgegebene Produkt über die richtige ID verfügt.

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

Beachten Sie, dass der Komponententest das Aktionsergebnis nicht ausführt. Sie können davon ausgehen, dass das Aktionsergebnis die HTTP-Antwort richtig erstellt. (Deshalb verfügt das Web-API-Framework über eigene Komponententests!)

Aktion gibt 404 (Nicht gefunden) zurück.

Die Get Methode ruft auf NotFound() , wenn das Produkt nicht gefunden wird. In diesem Fall überprüft der Komponententest nur, ob der Rückgabetyp NotFoundResult ist.

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

Aktion gibt 200 (OK) ohne Antworttext zurück.

Die Delete Methode ruft Ok() auf, um eine leere HTTP 200-Antwort zurückzugeben. Wie im vorherigen Beispiel überprüft der Komponententest den Rückgabetyp in diesem Fall 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));
}

Aktion gibt 201 (Created) mit einem Location-Header zurück

Die Post Methode ruft CreatedAtRoute auf, um eine HTTP 201-Antwort mit einem URI im Location-Header zurückzugeben. Überprüfen Sie im Komponententest, ob die Aktion die richtigen Routingwerte festlegt.

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

Aktion gibt einen weiteren 2xx mit einem Antwortkörper zurück.

Die Put Methode ruft Content auf, um eine HTTP 202 (Accepted)-Antwort mit einem Antworttext zurückzugeben. Dieser Fall ähnelt der Rückgabe von 200 (OK), aber der Komponententest sollte auch den Statuscode überprüfen.

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

Zusätzliche Ressourcen