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!