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!
Giovanni Bassi
Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.