ASP.NET Web API – Testes e Tracing

Israel Aece

Julho 2013

É importante testarmos a maioria dos códigos que escrevemos, e quando estamos falando em testes, não estamos necessariamente nos referindo sobre testes de alto nível, onde colocamos o usuário para realizar os testes. Nos referimos a testes automatizados, onde conseguimos escrever códigos para testar códigos, possibilitando a criação de diversos cenários para se certificar de que tudo funcione como esperado.

Como comentamos no decorrer dos capítulos anteriores, o ASP.NET Web API possibilita a construção de serviços de modelo tradicional, ou seja, definir tipos que refletem o nosso negócio (Cliente, Produto, Pedido, etc.), bem como tipos mais simples (inteiro, string, boleano, etc.). Como sabemos, a finalidade é conseguir desenhar um serviço que nada saiba sobre a infraestrutura, como ele é exposto, características, etc.

Ainda temos a possibilidade de receber e/ou retonar objetos que refletem e fazem uso de algumas informações do protocolo HTTP, que é um detalhe muito importante na estrutura REST. Ao utilizar as classes que descrevem a requisição (HttpRequestMessage) e a resposta (HttpResponseMessage), podemos interagir com detalhes do protocolo.

Não há muito mistério em aplicar testes em cima da classe que representa o serviço quando estamos lidando com tipos customizados. Isso se deve ao fato de que neste modelo, como são simples classes, com métodos que executam tarefas e, eventualmente, retornam algum resultado, isso acaba sendo tratado como sendo uma classe de negócio qualquer.

Mas e quando queremos receber e/ou enviar dados para este serviço, utilizando instâncias das classes HttpRequestMessage e HttpResponseMessage? Felizmente, assim como no ASP.NET MVC, a Microsoft desenvolveu o ASP.NET Web API com a possibilidade de testá-lo sem estar acoplado à infraestrutura do ASP.NET, o que permite testar a classe do serviço, mesmo que ela receba ou devolva objetos característicos do protocolo HTTP.

Supondo que temos um serviço que possui dois métodos (Ping e PingTipado), podemos escrever testes e, consequentemente, utilizar a IDE do Visual Studio para executá-los, e como isso, nos antecipamos à eventuais problemas que possam acontecer, pois talvez seja possível capturar alguns desses problemas antes mesmo de levar o mesmo ao ambiente de produção.

public class ServicoDeExemplo : ApiController
{
    public HttpResponseMessage Ping(HttpRequestMessage request)
    {
        return new HttpResponseMessage()
        {
            StatusCode = HttpStatusCode.OK,
            Content = new StringContent(request.Content.ReadAsStringAsync().Result)
        };
    }

    public HttpResponseMessage PingTipado(HttpRequestMessage request)
    {
        if (request.Content == null)
            return request.CreateErrorResponse(HttpStatusCode.BadRequest, 
                new HttpError("Conteúdo não definido"));

        return new HttpResponseMessage()
        {
            StatusCode = HttpStatusCode.OK,
            Content = 
                new ObjectContent<Informacao>(
                    request.Content.ReadAsAsync<Informacao>().Result, 
                    new JsonMediaTypeFormatter())
        };
    }
}

No primeiro exemplo, estamos testando o método Ping, instanciando a classe que representa o serviço, e passando ao método Ping a instância da classe HttpRequestMessage. Neste momento, poderíamos abastecer informações na coleção de headers da requisição, com o intuito de fornecer tudo o que é necessário para o que o método/teste possa executar com sucesso. Depois da requisição realizada, verificamos se o status da resposta corresponde ao status OK. Além disso verificamos também se o conteúdo da resposta está igual à informação que enviamos.

[TestMethod]
public void DadoUmaRequisicaoSimplesDeveRetornarStatusComoOK()
{
    var info = "teste";

    var response = new ServicoDeExemplo().Ping(new HttpRequestMessage()
    {
        Content = new StringContent(info)
    });

    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
    Assert.AreEqual(info, response.Content.ReadAsStringAsync().Result);
}

O próximo passo é construir um teste para o método PingTipado. Esse método recebe como parâmetro a instância da classe HttpRequestMessage, definindo o conteúdo uma instância da classe ObjectContent<T>, onde definimos o tipo genério T como sendo do tipo Informacao. A finalidade do teste é assegurar que, se passarmos uma instância nula da classe Informacao, uma resposta será retornada definindo o código de status como 400 (Bad Request).

[TestMethod]
public void DadoUmObjetoInfoNuloDeveRetornarComoErro()
{
    var response =
        new ServicoDeExemplo().PingTipado(new HttpRequestMessage());

    Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
}

Finalmente, o próximo passo consiste em criar um teste, também para o método PingTipado, mas agora fornecendo a instância do objeto Informacao em um estado válido, para que o teste possa suceder se o serviço retornar a mesma instância da classe Informacao, onde os membros da requisição reflitam o que está sendo retornado como reposta. Abaixo o código que efetua o tal teste:

[TestMethod]
public void DadoUmObjetoInfoDeveRetornarEleComInformacoesExtra()
{
    var info = new Informacao() { Codigo = "123", Dado = "Alguma Info" };
    var response =
        new ServicoDeExemplo()
            .PingTipado(new HttpRequestMessage()
            {
                Content =
                    new ObjectContent<Informacao>(info, new JsonMediaTypeFormatter())
            })
            .Content
            .ReadAsAsync<Informacao>().Result;

    Assert.AreEqual(info.Dado, response.Dado);
}

Para abstrair ainda o que está sendo testado, a Microsoft criou uma interface chamada IHttpActionResult, que encapsula o resultado dos retornos de ações. A implementação desta classe será responsável por criar a mensagem de retorno, e a ação dentro do controller passa a retornar uma classe que implemente esta interface, facilitando os testes unitários. O ASP.NET já está preparado para também entender este tipo resultado, processando o retorno normalmente.

Já temos nativamente cinco implementações: FormattedContentResult<T>, NegotiatedContentResult<T>, StatusCodeResult, ContinuationResult e MessageResult. Cada uma delas é responsável por receber um determinado tipo de resultado, prepará-lo e repassar para o sistema de testes ou para o pipeline ASP.NET um tipo genérico, que nada mais é que a instância da classe HttpResponseMessage.

public IHttpActionResult Get(string nome)
{
    var artista = 
        new Artista() { Id = 12, Nome = nome };

    return new FormattedContentResult<Artista>(
        HttpStatusCode.OK, 
        artista, 
        new JsonMediaTypeFormatter(), 
        new MediaTypeHeaderValue("application/json"), 
        this.Request);
}

Estamos recorrendo à classe FormattedContentResult<T> para retornar a instância da classe Artista no formato Json e com status de número 200 (OK). O ASP.NET entenderá o retorno normalmente, e quando estivermos criando os testes unitários para esta ação, independente do tipo de retorno que ela internamente definda (um objeto customizado, uma string, etc.), os testes sempre irão lidar a instância da classe HttpResponseMessage, e a partir dela, realizar todas as conferências necessárias para determinar se os testes executaram com sucesso ou não.

[TestMethod]
public void DeveRetornarRespostaCorreta()
{
    using (var request = new HttpRequestMessage())
    {
        var nome = "Max Pezzali";
        var response =
            new ArtistasController() { Request = request }
                .Get(nome)
                .ExecuteAsync(CancellationToken.None)
                .Result;

        Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
        Assert.AreEqual(nome, response.Content.ReadAsAsync<Artista>().Result.Nome);
    }
}

O que vimos até agora neste capítulo consiste em realizar os testes apenas nas classes que representam os serviços. Como os serviços REST usam o HTTP como parte do processo, muitas vezes somente a execução do controller é o suficiente para entender que ele foi executado com sucesso, o que nos obriga a recorrer à recursos do próprio HTTP para complementar a tarefa que está sendo executada e, consequentemente, também devemos compor isso em nossos testes.

Felizmente, pelo fato do ASP.NET Web API ser complemente desvinculado da infraestrutura, isso nos permite considerar os objetos que representam o “proxy” do cliente e o hosting do serviço nos testes, e validar se ao enviar, processar e retornar uma determinada requisição, se ela passa por todas os estágios de processamento dentro do pipeline do ASP.NET.

Os objetos HttpServer e o HttpClient foram construídos totalmente desvinculados de qualquer necessidade de somente executá-los em ambiente real. Com isso, podemos fazer uso destes mesmos objetos em um projeto de testes, onde podemos simular a mesma estrutura de objetos, suas configurações e seus interceptadores, que ao executar os testes, a requisição e resposta percorrerão todo o fluxo que percorreria quando ele for colocado em produção.

Para exemplificar isso, vamos considerar que temos um serviço que possui apenas dois métodos: um onde ele adiciona um objeto Cliente em um repositório qualquer, e outro que dado o Id deste Cliente, ele retorna o respectivo registro. Não vamos nos preocupar neste momento com boas práticas, mas no interior do controller podemos visualizar o repositório criado e sendo utilizado pelos dois métodos.

public class ClientesController : ApiController
{
    private static RepositorioDeClientes repositorio = new RepositorioDeClientes();

    [HttpGet]
    public Cliente Recuperar(int id)
    {
        return repositorio.RecuperarPorId(id);
    }

    [HttpPost]
    public HttpResponseMessage Adicionar(HttpRequestMessage request)
    {
        var cliente = request.Content.ReadAsAsync<Cliente>().Result;
        repositorio.Adicionar(cliente);

        var resposta = Request.CreateResponse<Cliente>(HttpStatusCode.Created, cliente);
        resposta.Headers.Location = 
            new Uri(string.Format("http://xpto/Clientes/Recuperar/{0}", cliente.Id));
        return resposta;
    }
}

Depois do serviço criado resta hospedarmos e consumirmos o mesmo através do projeto de testes.

[TestClass]
public class AcessoAosClientes
{
    private static HttpConfiguration configuracao;
    private static HttpServer servidor;
    private static HttpClient proxy;

    [ClassInitialize]
    public static void Inicializar(TestContext context)
    {
        configuracao = new HttpConfiguration();
        configuracao.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "{controller}/{action}/{id}"
        );

        servidor = new HttpServer(configuracao);
        proxy = new HttpClient(servidor);
    }

    [TestMethod]
    public void DeveSerCapazDeFazerPingComUmNovoRegistro()
    {
        var resultadoDaCriacao =
            proxy.PostAsync(
                "http://xpto/Clientes/Adicionar",
                new StringContent(
                    "{\"Nome\":\"Israel\", \"Cidade\":\"Valinhos\"}", 
                    Encoding.Default, "application/json"))
            .Result;

        Assert.AreEqual(HttpStatusCode.Created, resultadoDaCriacao.StatusCode);
        Assert.IsNotNull(resultadoDaCriacao.Headers.Location);

        var resultadoDaBusca = proxy.GetAsync(resultadoDaCriacao.Headers.Location).Result;
        var cliente = resultadoDaBusca.Content.ReadAsAsync<Cliente>().Result;

        Assert.AreEqual(1, cliente.Id);
        Assert.AreEqual("Israel", cliente.Nome);
        Assert.AreEqual("Valinhos", cliente.Cidade);
    }

    [ClassCleanup]
    public static void Finalizar()
    {
        proxy.Dispose();
        servidor.Dispose();
    }
}

As classes que representam o “proxy” e o hosting são declarados em nível de classe (teste). É interessante notar a construção destes objetos é realizada durante a incialização da classe que representa o teste. No construtor do hosting (HttpServer) recebe como parâmetro as configurações para o serviço; já a classe HttpClient recebe como parâmetro o HttpServer, para que internamente, quando solicitarmos a requisição para este cliente, ele encaminhe para o serviço. A URI aqui pouco importa, já que o tráfego será realizado diretamente. Isso é possível porque a classe HttpServer herda da classe HttpMessageHandler.

Dependências

Não há como falarmos de testes unitários sem que se tenha uma API que seja bem construída. As boas práticas pregam que uma classe não deve ter mais responsabilidade do que seu propósito, ou seja, se você tem uma API que expõe as músicas de um determinado álbum, ela (a API) deve coordenar como essa listagem será montada, mas não é responsabilidade dela conhecer detalhes, por exemplo, do banco de dados.

Ao desenhar uma classe, antes de colocar um código dentro dela, é necessário analisar se é ela quem deveria realizar essa atividade. Quanto mais a classe depender de uma abstração ao invés de uma implementação, será muito mais fácil substituir isso durante a escrita dos testes. No exemplo abaixo temos um controller que necessita de um repositório para extrair o álbum de um artista.

public interface IRepositorio
{
    Album BuscarAlbumPor(string artista);
}

public class ArtistasController : ApiController
{
    private readonly IRepositorio repositorio;

    public Artistas(IRepositorio repositorio)
    {
        this.repositorio = repositorio;
    }

    [HttpGet]
    public Album RecuperarAlbum(string artista)
    {
        return this.repositorio.BuscarAlbumPor(artista);
    }
}

Durante a escrita dos testes unitários, podemos criar e passar à classe Artistas uma representação em memória do repositório e, consequentemente, avaliar se o método RecuperarAlbum está atendendo o necessidade. A questão é como isso será realizado durante a execução da API.

Felizmente o ASP.NET Web API já possui internamente um local onde podemos adicionar todas as dependências do nosso controller, que durante a execução, ele será capaz de analisar a necessidade, construir o objeto, e entregá-lo à API para que seja utilizada. Para isso temos a interface IDependencyResolver (namespace System.Web.Http.Dependencies), qual podemos utilizar para customizar a criação dos controllers, onde poderemos abastecer manualmente toda a necessidade que cada um possui.

public class HardcodeResolver : IDependencyResolver
{
    public IDependencyScope BeginScope()
    {
        return this;
    }

    public object GetService(Type serviceType)
    {
        if (serviceType == typeof(ArtistasController))
            return new ArtistasController(new RepositorioXml("Artistas.xml"));

        return null;
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        return new List<object>();
    }

    public void Dispose() { }
}

Para que a classe HardcodeResolver funcione durante a execução, temos que apontar ao ASP.NET Web API que o objeto que criará a instância da classe que representará a API, resolverá todas as dependências e entregar para atender as requisições é ela. Novamente vamos recorrer ao objeto de configuração, que através da propriedade DependencyResolver podemos definir qualquer classe que implemente a interface IDependencyResolver.

config.DependencyResolver = new HardcodeResolver();

Os métodos BeginScope e Dispose são utilizados para controlar o tempo de vida dos objetos que são criados. Quando o controller ou qualquer objeto que ele seja capaz de resolver e criar é criado, podemos criar um objeto que define um escopo para ele, e após o runtime utilizá-lo, ele é devolvido para que seja adequadamente descartado, incluindo suas dependências internas que ela possa utilizar. Isso pode ser útil quando está utilizando algum container de inversão de controle (IoC). Se os objetos criados não tiverem a necessidade de gerenciamento de escopo para o descarte de recursos, então podemos retornar o this.

Tracing

Tracing é a forma que temos para monitorar a execução da aplicação enquanto ela está rodando. Isso é extremamente útil para diagnosticar problemas que ocorrem em tempo de execução, e que geralmente, por algum motivo específico faz com que a aplicação não se comporte como esperado.

O ASP.NET Web API já traz um mecanismo de captura extremamente simples de se trabalhar e tão poderoso quanto. Tudo acaba sendo realizado através da interface ITraceWriter (namespace System.Web.Http.Tracing), que dado uma implementação dela, o ASP.NET Web API captura detalhes referentes as mensagens HTTP e submete para que ela armazene no local de sua escolha.

Ele não vem com nenhuma implementação nativa, o que nos obriga a criar uma e acoplarmos à execução. Isso nos permitirá escolher qualquer meio de logging, como por exemplo o log4net, ETW, Logging Application Block, System.Diagnostics, etc. Esta interface fornece um único método chamado Trace, que recebe os seguintes parâmetros:

  • request: recebe o objeto HttpRequestMessage associado com as informações que serão coletadas.
  • category: uma string que determina a categoria em que as informação serão gravadas, permitindo agrupar informações que está relacionadas em pontos distintos da coleta.
  • level: um enumerador com as opções (já conhecidas) que definem o nível de severidade da informação.
  • traceAction: representa um delegate que permite ao chamador definir qualquer ação, que será executada quando o mecanismo de trace decidir coletar alguma informação.

Para termos uma ideia das informações que são coletadas, abaixo temos um logging que se exibe as informações em uma aplicação console. A utilização da aplicação console é para mostrar o funcionamento do mecanismo, mas como já falado acima, poderíamos criar várias implementações. Quando estamos lidando com aplicações do mundo real, é necessário recorrermos a alguma biblioteca já existente e que faça grande parte do trabalho para armazenar e, principalmente, forneça uma forma simples para monitorar.

public class ConsoleLogging : ITraceWriter
{
    public void Trace(HttpRequestMessage request, string category, 
        TraceLevel level, Action<TraceRecord> traceAction)
    {
        var record = new TraceRecord(request, category, level);
        traceAction(record);

        View(record);
    }

    private void View(TraceRecord record)
    {
        Console.WriteLine(record.RequestId);
        Console.WriteLine("{0} - {1}", record.Category, record.Level);
        Console.WriteLine("{0} - {1}", record.Request.Method, record.Request.RequestUri);
        Console.WriteLine("{0} - {1}", record.Operator, record.Operation);
        Console.WriteLine();
    }
}

A classe TraceRecord representa um item de rastreamento e é ele que deve ser catalogado para futuras análises. No interior do método Trace construímos o objeto TraceRecord e antes de passarmos para o delegate traceAction, podemos customizar com informações específicas. E no exemplo acima, depois de configurado o TraceRecord, exibimos as propriedades deste projeto na console:

Dn376309.0EAC57E78D15E4D64F4277D8B6204978(pt-br,MSDN.10).png

Figura 20 - Logs sendo exibidos na console.

É claro que a implementação não é suficiente para que tudo isso funcione. Para que ele seja acionado, é necessário acoplarmos à execução, e para isso, recorremos ao objeto de configuração do ASP.NET Web API. Neste momento, tudo o que precisamos saber para que o logging customizado funcione é adicionar o seguinte comando na configuração da API:

config.Services.Replace(typeof(ITraceWriter), new ConsoleLogging());

E para finalizar, como pudemos perceber, somente informações inerentes aos estágios do processamento da requisição foram logados. E se desejarmos também incluir informações referentes as regraas de negócio, ou melhor, incluir informações que são geradas no interior do controller? A classe ApiController possui uma propriedade chamada Configuration, que expõe o objeto de configuração a API e, consequentemente, nos permite acessar o tracing para incluir qualquer informação que achemos importante e necessário para quando precisarmos monitorar.

Para facilitar a inserção destas informações customizadas, a Microsoft incluiu uma classe estática com métodos de estensões à interface ITraceWriter com métodos nomeados com as severidades.

using System.Web.Http;
using System.Web.Http.Tracing;

public class TestController : ApiController
{
    public string Get()
    {
        this.Configuration
            .Services
            .GetTraceWriter()
            .Info(this.Request, "Ações", "Alguma Info", "xpto");

        return "test";
    }
}

Dn376309.76DB71EF7D8B124CAF29C9EC78D3DA24(pt-br,MSDN.10).png

Figura 21 - Informação customizada sendo exibida.

Veja também:

ASP.NET Web API – HTTP, REST e o ASP.NET: Para basear todas as funcionalidades expostas pela tecnologia, precisamos ter um conhecimento básico em relação ao que motivou tudo isso, contando um pouco da história e evolução, passando pela estrutura do protocolo HTTP e a relação que tudo isso tem com o ASP.NET.

ASP.NET Web API – Estrutura da API: Entenderemos aqui a template de projeto que o Visual Studio fornece para a construção das APIs, bem como sua estrutura e como ela se relaciona ao protocolo.

ASP.NET Web API – Roteamento: Como o próprio nome diz, o capítulo irá abordar a configuração necessária para que a requisição seja direcionada corretamente para o destino solicitado, preenchendo e validando os parâmetros que são por ele solicitado.

ASP.NET Web API – Hosting: Um capítulo de extrema relevância para a API. É o hosting que dá vida à API, disponibilizando para o consumo por parte dos clientes, e a sua escolha interfere diretamente em escalabilidade, distribuição e gerenciamento. Existem diversas formas de se expor as APIs, e aqui vamos abordar as principais delas.

ASP.NET Web API – Consumo: Como a proposta é ter uma API sendo consumido por qualquer cliente, podem haver os mais diversos meios (bibliotecas) de consumir estas APIs. Este capítulo tem a finalidade de exibir algumas opções que temos para este consumo, incluindo as opções que a Microsoft criou para que seja possível efetuar o consumo por aplicações .NET.

ASP.NET Web API – Formatadores: Os formatadores desempenham um papel importante na API. São eles os responsáveis por avaliar a requisição, extrair o seu conteúdo, e quando a resposta é devolvida ao cliente, ele entra em ação novamente para formatar o conteúdo no formato em que o cliente possa entender. Aqui vamos explorar os formatadores padrões que já estão embuitdos, bem como a criação de um novo.

ASP.NET Web API – Segurança: Como a grande maioria das aplicações, temos também que nos preocupar com a segurança das APIs. E quando falamos de aplicações distribuídas, além da autenticação e autorização, é necessário nos preocuparmos com a segurança das mensagens que são trocadas entre o cliente e o serviço. Este capítulo irá abordar algumas opções que temos disponíveis para tornar as APIs mais seguras.

ASP.NET Web API – Testes e Tracing: Para toda e qualquer aplicação, temos a necessidade de escrever testes para garantir que a mesma se comporte conforme o esperado. Isso não é diferentes com APIs Web. Aqui iremos abordar os recursos, incluindo a própria IDE, para a escrita, gerenciamento e execução dos testes.

ASP.NET Web API – Estensibilidade e Arquitetura: Mesmo que já tenhamos tudo o que precisamos para criar e consumir uma API no ASP.NET Web API, a customização de algum ponto sempre acaba sendo necessária, pois podemos criar mecanismos reutilizáveis, “externalizando-os” do processo de negócio em si. O ASP.NET Web API foi concebido com a estensibilidade em mente, e justamente por isso que existe um capítulo exclusivo para abordar esse assunto.

| Home | Artigos Técnicos | Comunidade