Testes integrados com WebAPI

Vocês já devem estar sabendo que o WebAPI vai fazer parte da nova versão do ASP.NET MVC (apesar de ser independente dele). Saiba mais sobre ele aqui. Estou em um projeto usando este framework, que sustenta a ideia de “nova web” que discuti com o Victor Cavalcante no Tecnoretórica.

Uma coisa legal é que os caras facilitaram muito o cenário de testes. É possível testar um controller do WebAPI sem precisar colocar o servidor no ar. Há uma classe HttpServer, que pode hospedar os requests, e há o HttpClient, que faz requests. Se você passar o server para o client, você consegue testar tudo sem nunca abrir uma porta do servidor, e tudo em memória.

Nesse exemplo eu criei um projeto de testes do próprio MSTest, e criei os controllers no próprio projeto de testes. O projeto está no github, pra quem quiser olhar ele mais de perto.

Temos então esse controller, super simples:

public class ConjuntosController : ApiController
{
    [HttpGet]
    public object Tail(int[] ns)
    {
        if (ns.Length == 0)
        {
            return new {mensagem = "Tail vazio"};
        }
        return ns.Skip(1).ToArray();
    }
}

Ele tem um único método, e como podem ver, ele retorna o final de um array, o conhecido método Tail. Por definição, tail não responde a um conjunto vazio. Nesse caso ele retorna uma mensagem.

Eu poderia realizar um teste de unidade. Mas isso não garantiria, por exemplo, que o projeto está atendendo minhas necessidades de integração. Eu posso querer saber se ele, por exemplo, está retornando Json conforme o esperado.

Para isso eu tenho que colocar um servidor no ar, passar Json e especificar que quero receber Json de volta.

Criei uma classe base para fazer isso, e deixei para o meu teste só o que importa. Aqui está o teste para dois elementos do conjunto:

[TestMethod]
public void TailComDoisElementos()
{
    using (var cliente = new HttpClient(servidor))
    {
        var conteudo = new[] {1, 2, 3};
        using (var request = CriarRequest("api/Conjuntos/Tail", HttpMethod.Get, conteudo))
        {
            using (var response = cliente.SendAsync(request, new CancellationTokenSource().Token).Result)
            {
                var retorno = response.Content.ReadAsStringAsync().Result;
                retorno.Should().Be(JsonConvert.SerializeObject(new[] {2, 3}));
                retorno.Should().Be("[2,3]");
            }
        }
    }
}

 

Note que ele não instancia o controller diretamente. No teste, eu crio um HttpClient, que chama “api/Conjuntos/Tail”. É essa string que especifica que o ConjuntosController tem que ser chamado, com o método Tail. O retorno é assíncrono, lógico, e o resultado é devolvido em Json, porque esse é o formato esperado (depois mostarei onde).

As linhas 12 e 13 fazem a mesma coisa. Eu coloquei as duas pra que ficasse evidente o que estou comparando na linha 12, onde, na verdade, converto o objeto para Json e confirmo se é a string correta para o Json que espero.

Se eu passar um array vazio o controller me devolve uma mensagem. Aqui estão as 3 linhas que mudam no assert:

retorno.Should().Be(JsonConvert.SerializeObject(new {mensagem = "Tail vazio"}));
retorno.Should().Be("{\"mensagem\":\"Tail vazio\"}");
Assert.AreEqual("Tail vazio", (string)((dynamic)JsonConvert.DeserializeObject(retorno)).mensagem);
Assert.AreEqual("Tail vazio", (string)retorno.DeserializarJson().mensagem);

 

As linhas 1 e 2 testam a mesma coisa, mais uma vez, estou só demonstrando o que esperava da string Json.

Na terceira linha estou fazendo o oposto do que faço na primeira. Eu desserializo o objeto, e chamo dinamicamente a propriedade “mensagem”, e comparo se ela tem a mensagem que eu esperava. O Json.NET já retornasse dynamic isso seria mais fácil, e pra facilitar isso eu usei um método de extensão que criei que já fazia isso. Outra opção seria usar o JsonFx, que faz isso ainda melhor.

Eu criei alguns métodos para me ajudar, aqui estão eles, em uma classe base, abstrata. Aqui estão as partes que interessam (não deixe de ver o restante no github se quiser aprofundar):

protected HttpServer servidor;
protected const string urlBase = "http://algumaurl.com/";

[TestInitialize]
public void Setup()
{
    var config = new HttpConfiguration { IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always };
    /*outras rotas...*/
    config.Routes.MapHttpRoute(name: "ActionApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional });
    servidor = new HttpServer(config);
}

protected HttpRequestMessage CriarRequest(string url, HttpMethod method, object content = null, string mediaType = "application/json", MediaTypeFormatter formatter = null)
{
    var request = new HttpRequestMessage { RequestUri = new Uri(urlBase + url), Method = method };
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mediaType));
    if (content!=null) 
        request.Content = new ObjectContent(content.GetType(), content, formatter ?? new JsonMediaTypeFormatter());
    return request;
}

Basicamente antes de rodar o teste eu crio um HttpServer com as configurações básicas. A url de base não faz a menor diferença, você usa o que achar melhor. E para criar o request, basta criar uma mensagem com a url correta, e, se tiver algum conteúdo, passar para a propriedade Content do HttpRequestMessage.

Da forma como está esse é um teste em memória super rápido. Mas se o controller chamasse serviços de infraestrutura, o que é o mais comum, já demoraria mais. Não se enganem: é um teste de integração, e de caixa preta. É muito útil para testar filtros, error handlers, etc, além dos testes de serialização, que foi o que demonstrei aqui. Você não consegue mockar de maneira fácil as dependências do controller. Mas dá. Isso fica para um próximo post.

Compare agora com a dor de cabeça de fazer isso com serviços ASMX, WCF, ou até com controllers ASP.NET MVC. Muito mais fácil!

Divirtam-se!

  • Igor Moreira

    Muito bom o post. Parabéns.