Controladores de teste de unidade no ASP.NET Web API 2

por Mike Wasson

Este tópico descreve algumas técnicas específicas para controladores de teste de unidade na API Web 2. Antes de ler este tópico, talvez você queira ler o tutorial teste de unidade ASP.NET Web API 2, que mostra como adicionar um projeto de teste de unidade à sua solução.

Versões de software usadas no tutorial

Note

Usei MOQ, mas a mesma ideia se aplica a qualquer estrutura fictícia. O MOQ 4.5.30 (e posterior) dá suporte ao Visual Studio 2017, Roslyn e .NET 4,5 e versões posteriores.

Um padrão comum em testes de unidade é " Arrange-Act-Assert " :

  • Organizar: configure todos os pré-requisitos para que o teste seja executado.
  • Act: execute o teste.
  • Assert: Verifique se o teste foi bem-sucedido.

Na etapa organizar, você geralmente usará objetos fictícios ou de stub. Isso minimiza o número de dependências, de modo que o teste se concentra em testar uma coisa.

Aqui estão algumas coisas que você deve testar unidade em seus controladores de API Web:

  • A ação retorna o tipo correto de resposta.
  • Parâmetros inválidos retornam a resposta de erro correta.
  • A ação chama o método correto no repositório ou na camada de serviço.
  • Se a resposta incluir um modelo de domínio, verifique o tipo de modelo.

Essas são algumas das coisas gerais a serem testadas, mas as especificações dependem da implementação do controlador. Em particular, faz uma grande diferença se as ações do controlador retornarem HttpResponseMessage ou IHttpActionResult. Para obter mais informações sobre esses tipos de resultado, consulte resultados da ação na API da Web 2.

Ações de teste que retornam HttpResponseMessage

Aqui está um exemplo de um controlador cujas ações retornam 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;
    }
}

Observe que o controlador usa injeção de dependência para injetar um IProductRepository . Isso torna o controlador mais testado, pois você pode injetar um repositório fictício. O teste de unidade a seguir verifica se o Get método grava um Product no corpo da resposta. Suponha que repository seja uma simulação 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);
}

É importante definir a solicitação e a configuração no controlador. Caso contrário, o teste falhará com um ArgumentNullException ou InvalidOperationException.

O Post método chama UrlHelper. link para criar links na resposta. Isso requer um pouco mais de configuração no teste de unidade:

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

A classe UrlHelper precisa da URL de solicitação e dos dados de rota, portanto, o teste precisa definir valores para eles. Outra opção é imitação ou UrlHelperde stub. Com essa abordagem, você substitui o valor padrão de ApiController. URL por uma simulação ou versão de stub que retorna um valor fixo.

Vamos reescrever o teste usando a estrutura MOQ . Instale o Moq pacote NuGet no projeto de teste.

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

Nesta versão, você não precisa configurar nenhum dado de rota, pois o UrlHelper de simulação retorna uma cadeia de caracteres constante.

Ações de teste que retornam IHttpActionResult

Na API da Web 2, uma ação do controlador pode retornar IHttpActionResult, que é análogo ao ACTIONRESULT no ASP.NET MVC. A interface IHttpActionResult define um padrão de comando para criar respostas http. Em vez de criar a resposta diretamente, o controlador retorna um IHttpActionResult. Posteriormente, o pipeline invoca o IHttpActionResult para criar a resposta. Essa abordagem facilita a gravação de testes de unidade, pois você pode ignorar muitas das configurações necessárias para o HttpResponseMessage.

Aqui está um controlador de exemplo cujas ações retornam 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);
    }    
}

Este exemplo mostra alguns padrões comuns usando IHttpActionResult. Vamos ver como testar as unidades.

A ação retorna 200 (OK) com um corpo de resposta

O Get método chamará Ok(product) se o produto for encontrado. No teste de unidade, verifique se o tipo de retorno é OkNegotiatedContentResult e se o produto retornado tem a ID correta.

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

Observe que o teste de unidade não executa o resultado da ação. Você pode assumir que o resultado da ação cria a resposta HTTP corretamente. (É por isso que a estrutura da API Web tem seus próprios testes de unidade!)

A ação retorna 404 (não encontrado)

O Get método chamará NotFound() se o produto não for encontrado. Para esse caso, o teste de unidade apenas verifica se o tipo de retorno é 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));
}

A ação retorna 200 (OK) sem corpo de resposta

O Delete método chama Ok() para retornar uma resposta http 200 vazia. Como no exemplo anterior, o teste de unidade verifica o tipo de retorno, neste caso, 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));
}

A ação retorna 201 (criado) com um cabeçalho de local

O Post método chama CreatedAtRoute para retornar uma resposta http 201 com um URI no cabeçalho Location. No teste de unidade, verifique se a ação define os valores de roteamento corretos.

[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 ação retorna outro 2xx com um corpo de resposta

O Put método chama Content para retornar uma resposta http 202 (aceito) com um corpo de resposta. Esse caso é semelhante a retornar 200 (OK), mas o teste de unidade também deve verificar o código de status.

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

Recursos adicionais