Freigeben über


Komponententests für Controller 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 Tutorial Unit Testing ASP.NET-Web-API 2 lesen, in dem gezeigt wird, wie Sie Ihrer Projektmappe ein Komponententestprojekt hinzufügen.

Im Tutorial verwendete Softwareversionen

Hinweis

Ich habe Moq verwendet, aber die gleiche Idee gilt für jedes Modellframework. 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 in Komponententests ist "arrange-act-assert":

  • Anordnen: Richten Sie alle Voraussetzungen für die Ausführung des Tests ein.
  • Act: Führen Sie den Test aus.
  • Assert: Überprüfen Sie, ob der Test erfolgreich war.

Im Schritt "Anordnen" verwenden Sie häufig Pseudo- oder Stubobjekte. Dadurch wird die Anzahl der Abhängigkeiten minimiert, sodass sich der Test auf das Testen einer Sache konzentriert.

Im Folgenden finden Sie einige Punkte, 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 für das Repository oder die Dienstebene auf.
  • Wenn die Antwort ein Domänenmodell enthält, überprüfen Sie den Modelltyp.

Dies sind einige der allgemeinen Dinge, die getestet werden müssen, aber die Besonderheiten hängen von Ihrer 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 Web-API 2.

Testen von Aktionen, die HttpResponseMessage zurückgeben

Hier 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 die Abhängigkeitsinjektion verwendet, um ein zu injizieren IProductRepository. 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. Angenommen, es handelt sich repository um eine Simulierte 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);
}

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

Die Post -Methode ruft UrlHelper.Link auf, um Links 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 weitere Option ist Mock- oder Stub-UrlHelper. Bei diesem Ansatz ersetzen Sie den Standardwert von ApiController.Url durch eine Modell- oder Stubversion, 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 der simulierte UrlHelper eine konstante Zeichenfolge zurückgibt.

Testen von Aktionen, die IHttpActionResult zurückgeben

In Web-API 2 kann eine Controlleraktion IHttpActionResult zurückgeben, was analog zu ActionResult in ASP.NET MVC 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 einen Großteil der einrichtung überspringen können, die für HttpResponseMessage erforderlich ist.

Hier sehen Sie einen 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 sie komponententesten.

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 ordnungsgemäß 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 auf Ok() , 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 (Erstellt) mit einem Location-Header zurück.

Die Post -Methode ruft auf CreatedAtRoute , 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 eine weitere 2xx mit einem Antworttext zurück.

Die Put -Methode ruft auf Content , um eine HTTP 202-Antwort (Akzeptiert) mit einem Antworttext zurückzugeben. Dieser Fall ähnelt der Rückgabe von 200 (OK), aber der Komponententest sollte auch den status Code ü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