Sobre como testamos
Uma pergunta: Por que testamos? Por que fazemos testes? Me refiro a testes automatizados, desde o ponto de vista do desenvolvedor. De qualquer tipo. Pode ser unitários, de aceite, de integração ou até de performance. Quase todos passamos algum momento na nossa vida onde pensávamos que não era preciso testar nosso código. Porém, agora testamos como loucos, convencidos de que são necessários. Ou não?
Testar ou não testar
Então vamos responder a primeira pergunta, desde o ponto de vista do developer. Por que testamos?
- Porque o contrato marca que devemos atingir uma cobertura de linhas?
Desse ponto de vista, só precisamos criar testes unitários que passem por todas as linhas e não que realmente testem o código. Por exemplo, se ainda não usa lombok , pode testar getters e setters e já estará perto da meta de cobertura de linhas. Mas isso não garante qualidade. - Porque está na moda?
É verdade, está na moda. Na próxima entrevista de emprego já sabe o que tem que falar: eu testo meus desenvolvimentos, isso é muito importante porque garanto melhor o funcionamento e diminuo os bugs em produção. Beleza, mas vamos mais no fundo. - Porque há uma relação custo-beneficio com o momento em que achamos os defeitos?
Bom, estamos chegando no ponto. Achar um bug em produção é entre 10 e 1000 vezes mais custoso que em fase de desenvolvimento. Não tenho dados estatísticos na mão, mas nesse post tem um pouco mais de informação.
Todas as respostas são válidas. Mas, o que realmente queremos é dormir tranquilos. Podemos, claro, testar nossa aplicação manualmente e garantir todos os fluxos. Ou melhor, ter um time dedicado a testar a aplicação. Se estamos desenvolvendo a feature A, podemos (ou o time de teste pode) criar nossos roteiros de teste, como todos os cenários imagináveis, e garantir a entrega. Vamos dormir tranquilos, até que entra a feature B, que pode ter alguma incidência na feature A… Deveremos retestar o roteiro de teste de feature A. Okay. Depois de 10 meses de desenvolvimento, o 90% do tempo vai estar fazendo testes de regressão (ou vai ter que ampliar o time de teste), diminuindo a produtividade do time. Então, para dormir relativamente tranquilos e não ter que testar manualmente tudo de novo, precisamos automatizar. Então, vamos mudar a pergunta.
Como testar
Já não estamos perguntando se precisamos testar. Sabemos que precisamos testar. Todos queremos dormir tranquilos. E todos queremos desenvolver features e não repetir testes de regressão dia após dia. Mas, e ai?
- Saímos fazendo testes unitários que nem loucos. Poderia ser uma opção. Mas se uma parte da aplicação se integra com outra (por exemplo, a camada de serviço, se integra com o repositório, que a sua vez se integra com o banco de dados), não acha interessante testar isso também?
- Então vamos fazer testes de integração também, para todos os pontos do código. Hummm, beleza. Você vai garantir suas entregas. Será que é sustentável? Quantas vezes precisou alterar o teste unitário porque o código quebrava, mas a funcionalidade era a mesma?
Talvez a resposta seja um pouco menos binária. Porque os caras de TI somos binários. Ou é, ou não é. Só que as vezes é melhor aplicar o bom senso. Sua camada de serviço só pega o VO que chega da tela, cria um entity a partir dele e manda para o repositório. Faz sentido testar isso unitariamente? Provavelmente não.
Behavior Driven Development
Outro problema, é que fazemos testes para nós, com nomes de teste ilegíveis, normalmente em inglês. Quando o teste quebrar dentro de 5 meses, ninguém sabe o que realmente faz nem porque. Até pode ser que quem desenvolveu já não esteja na empresa. O Behavior Driven Development (ou BDD para os amigos) nos fornece uma alternativa. Basicamente, baseamos nosso desenvolvimento em função do comportamento. E para nós nerds de TI, é bom. Trazemos a definição do comportamento antes de começar a desenvolver, esclarecendo certos cenários e visualizando outros problemas antes de começar. Então, já nessa fase, antecipamos bugs ao facilitar a identificação de incongruências ou outras falhas. E desenvolvemos os testes em português, o que nos permite identificar qual é a funcionalidade que falhou de um modo mais humano. Um exemplo:
Cenário: Login com senha inválida
Dado que o usuário acesse a página de login
E exista o usuário “iundarigun” no sistema com a senha “senha_iundarigun”
E digite o usuário “iundarigun” e a senha “incorreta_para_iundarigun”
Quando clicar no botão de logar
Então o sistema retorna mensagem de “usuário ou senha incorretos”.
Ainda podemos usar isso como um tipo de documentação viva, pois se os testes passar, significa que a documentação é válida! E provavelmente, o envolvimento de um especialista em Qualidade pode ser mais global, e não só no final executando uma roteiro chato. Agora passa a formar parte fundamental do time.
A metodologia não está isenta de certa polêmica. Esse tipo de definição é uma linguagem chamada Gherkin, que ajuda na implementação de testes de Cucumber. Agora, o uso dessa linguagem não garante o uso de BDD. Veja o seguinte exemplo:
Cenário: Login com senha inválida no service
Dado que o VO de entrada com user “iundarigun” e senha “incorreta_para_iundarigun”
E a validação do DAO para o usuário “iundarigun” volta “senha_iundarigun”
Quando chamar o service validarLogin
Então deve retornar a exception “usuário ou senha incorretos”.
O que temos aqui é um teste unitário fantasiado de BDD. Até indicamos o mock da validação do DAO. Claro, isso é uma extrapolação, mas entendeu o ponto. Isso é errado? Depende. Atende sua necessidade? É melhor para o seu projeto fantasiar os testes unitários desse jeito para serem mais legíveis? Vá enfrente. Mas tenha claro que não é isso o que estamos falando.
Testes Unitários vs BDD
O BDD em nenhum caso substitui os testes unitários, mas os complementa e permite parar de fazer testes unitários desnecessários. Não existe uma proporção correta entre os diferentes tipos de testes. Depende, principalmente, do projeto. Um dos projetos onde atuamos, a proporção deve estar em volta de 20% unitários, 80% BDD. E sem testes de front. No caso específico, o uso do mesmo é bem restrito, só uns poucos usuários internos, telas simples. Mas em outros, a distribuição é totalmente diferente. E isso é o bonito. Não existe bala de prata. É bom parar de desenvolver e avaliar. Preciso fazer testes unitários aqui? Preciso testar front? Preciso BDD? Qual é o custo de determinado tipo de teste, e qual é o risco de não fazê-lo?
Show me the code!
Não é o objetivo deste post descrever ao detalhe como usar o BDD em Java, mas para matar a vontade, vamos descrever a seguinte funcionalidade. Temos uma aplicação Rest que permite a pesquisa de de produtos da nossa loja. Disponibilizamos uma pesquisa por categoria, por preço e por identificador. O código está no github:
> git clone https://github.com/iundarigun/testing-product.git
Para simplificar, só testamos a pesquisa por id. Uma possível definição da funcionalidade é a seguinte:
#language: pt
Funcionalidade: Pesquisa de produtosCenário: Pesquisa por id sem id
Dado que o usuário deseje consultar pelo identificador
Quando acionar a consulta com id vazio
Então a consulta deve retornar uma exceçãoCenário: Pesquisa por id inválido
Dado que o usuário deseje consultar pelo identificador
Quando acionar a consulta com id "AAA"
Então a consulta deve retornar uma exceçãoCenário: Pesquisa por id inexistente
Dado que o usuário deseje consultar pelo identificador
Quando acionar a consulta com id "95"
Então a consulta não deve retornar produtosCenário: Pesquisa por id válido
Dado que o usuário deseje consultar pelo identificador
Quando acionar a consulta com id "1"
Então a consulta deve retornar o produto válido
E o nome do produto deve ser "Moto G5 Plus"
E a categoria do produto deve ser "telefonia"
Estamos definindo o comportamento da nossa aplicação, desde os casos não permitidos até os casos de sucesso. E define até o ponto de decidir se uma consulta por id inexistente deve lançar uma exceção ou não, o que, de certa forma, vira uma documentação da nossa aplicação.
Nossa classe de teste fica assim:
@ContextConfiguration(classes = ProductApplication.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.DEFINED_PORT)
public class PesquisaProdutosStepDefs {private String url;
private String id;
private String result;
private Exception exception;
private Product product;@Before
private void setup(){
this.url = null;
this.id = null;
this.result = null;
this.exception = null;
this.product = null;
}@Dado("^que o usuário deseje consultar pelo identificador$")
public void queOUsuárioDesejeConsultarPeloIdentificador() throws Throwable {
this.url = "http://localhost:8181/products/id/";
}@Quando("^acionar a consulta com id vazio$")
public void acionarAConsultaComIdVazio() throws Throwable {
this.id = "";
consultaPorId();
}private void consultaPorId(){
RestTemplate restTemplate = new RestTemplate();
try {
this.result = restTemplate.getForObject(this.url + id, String.class);
}
catch(Exception e){
this.exception = e;
}
}@Então("^a consulta deve retornar uma exceção$")
public void aConsultaDeveRetornarUmaExceção() throws Throwable {
Assert.assertNotNull(exception);
}@Quando("^acionar a consulta com id \"([^\"]*)\"$")
public void acionarAConsultaComId(String id) throws Throwable {
this.id = id;
consultaPorId();
}@Então("^a consulta não deve retornar produtos$")
public void aConsultaNãoDeveRetornarProdutos() throws Throwable {
Assert.assertNull(exception);
Assert.assertNull(result);
}@Então("^a consulta deve retornar o produto válido$")
public void aConsultaDeveRetornarOProdutoVálido() throws Throwable {
Assert.assertNull(exception);
Assert.assertNotNull(result);
ObjectMapper mapper = new ObjectMapper();
this.product = mapper.readValue(result, Product.class);
Assert.assertNotNull(this.product);
}@E("^o nome do produto deve ser \"([^\"]*)\"$")
public void oNomeDoProdutoDeveSer(String nome) throws Throwable {
Assert.assertEquals(nome,product.getName());
}@E("^a categoria do produto deve ser \"([^\"]*)\"$")
public void aCategoriaDoProdutoDeveSer(String categoria) throws Throwable {
Assert.assertNotNull(product.getCategory());
Assert.assertEquals(categoria,product.getCategory().getName());
}
}
Nesse caso estamos considerando o banco já populado com dados preparados. Pode ver o import.sql na raiz dos resources de testes.
Encerrando
Claro que vocês podem pensar que faltam alguns tipos de teste. Por exemplo, testes de frontend com Selenium ou usando outras tecnologias com PhantonJS ou Jasmine. Depende do cenário. Os projetos onde aplicamos o BDD, não sentimos a necessidade de tratar o frontend, pois as telas eram simples e as regras de negócio estavam no backend, mas no próximo projeto pode ser necessário. E isso é o mais importante, aplicar o senso crítico, avaliar o que atende o seu cenário, avaliar o que vai deixar dormir as noites.