Perdendo ou ganhando tempo com testes de unidade

Durante as aulas e palestras sobre TDD e testes de software é bem comum ouvir perguntas relativas a “o que deve ser testado e o que não precisa ser testado”. Geralmente os exemplos inicials que encontramos na literatura sobre TDD são muito simplistas, nos levando a crer que devemos testar todo e qualquer método de uma classe de maneira isolada, mesmo que não faça muito sentido.

Um exemplo desse tipo de abordagem é a tentativa de se testar uma constante, entre outros clássicos como a presença de uma anotação, métodos de delegação, getters e setters, etc. Supondo uma classe que armazena constantes de cartão de crédito e um possível teste de unidade para ela:

public class CartoesDeCreditoTest {
  @Test
  public void deveRelacionarVisaComOValor123() {
    assertEquals(123, CartoesDeCredito.VISA);
  }
}

public class CartoesDeCredito {
  public final static int VISA = 123;
  public final static int MASTERCARD = 456;
}

Repare que existe uma duplicação de dados entre o código de teste e o código de produção: o número 123. Quando o valor dessa constante mudar, você possui dois pontos de código para alterar. Ao fazer uma refatoração para remover essa duplicação, teremos um resultado como a seguir:

public class CartoesDeCreditoTest {
  @Test
  public void deveRelacionarVisaComOValor123() {
    assertEquals(CartoesDeCredito.VISA, CartoesDeCredito.VISA);
  }
}

Muitas vezes, o programador, acostumado a escrever testes de unidade para todo o código, acaba desenvolvendo testes sem utilidade: código que na realidade não testa nada. Naturalmente, o desenvolvedor não faria a refatoração acima, tal exemplo demonstra como um teste do genêro não traz valor, somente complexidade para nosso sistema.

Ou seja, em casos como o mostrado acima não há utilidade em escrever um teste de unidade para constantes. Ao fazer isso, o programador gera duplicação de dados entre a classe de teste e a de produção.

É importante testar constantes, mas no caso acima por exemplo, não através de testes de unidade: se ela representa um valor de um outro sistema, por exemplo, escreva um teste de integração.

No caso do cartão de crédito, suponha que a constante 123 seja o código do pagamento via cartão VISA no sistema do PayPal, portanto testamos a integração entre a aplicação e o sistema externo. Verifique se, ao passar o valor 123, o pagamento gerado foi um pagamento VISA.

@Test
public void deveEnviarCartaoVisaParaOPayPal() {
  Cobranca resultado = payPal.efetuaCobranca(valor, numeroDeUmCartaoVisa, CartoesDeCredito.VISA);

  assert(that(resultado.getStatus()).is(PayPal.APROVADO));
}

Em um exemplo mais elaborado, imagine agora o seguinte trecho de código abaixo que usa o Active Record do Rails para fazer um filtro:

def filtro(name)
  where("nome like %#{name}%")
end

def test_verifica_se_where_foi_invocado
  componente.should_receive(:where).with("nome like 'guilherme'")
  componente.filtro("guilherme")
end

# outra variação do mesmo teste:

def test_verifica_se_where_foi_invocado
  query = componente.filtro("guilherme")
  query[0].should == "nome like 'guilherme'"
end

Neste caso, uma vez que o método possui um corpo simples, uma invocação de uma função do Active Record, o teste abusa de mocks e é apenas uma mímica daquilo que o método executou. Ele garante somente que o programador digitou aquilo que ele esperava digitar. Testes como esse só indicam um problema no design do teste ou um problema no design do código.

No mundo Java é comum ver similares quando encontramos a utilização de Mocks ao extremo:

public void adiciona(Produto p) {
  this.session.save(p);
}

@Test
public void testSalvarEhInvocado() {
  Produto produto = new Produt();
  one(session).save(produto);
  dao.adiciona(produto);
}

Veja que o próprio nome do método diz que ele não serve pra nada: verifica se o salvar é invocado. Você quer testar o comportamento que essa classe terá sobre seu sistema e não se um determinado método foi invocado. A obviedade que o método está sendo invocado é trivial de ser notada e por isso mesmo não precisaria ser nosso foco no teste.

Mas alguns podem dizer: mas não preciso testar o Active Record, eu sei que ele funciona. Ou ainda, não preciso testar o Hibernate, eu sei que ele funciona. Sim, você não precisa testar o Active Record unitariamente pois ele já foi testado, mas existe a necessidade de testar a integração do seu código com o Active Record. No exemplo de uma query, sabemos que escrevemos uma query como desejávamos, mas ela está retornando aquilo que desejamos? E deixando de lado o resto? São essas perguntas que nosso teste precisa responder.

def test_deve_retornar_pessoas_que_tenham_parte_do_nome
  lista = executa_metodo_where("joão");

  lista.should_contain o_cidadao("João Gilberto")
end

O mesmo vale para queries em Java que utiliza JPA/Hibernate.

Para saber mais sobre teste e design: Kent Beck já discutia acoplamento tanto de dados quanto de código entre classes de teste e classes de produção. Isso é muitas vezes ignorado pelo programador. Imagine que você precise alterar muitos testes toda vez que fizer uma pequena alteração: é um indício de um design rígido, onde uma simples alteração propaga uma série de outras alterações.

Como regra geral, que possui suas excecões, não existe valor em um teste que é uma mímica daquilo que foi digitado e não de seus efeitos e retornos. Nesse caso, pense duas vezes antes de escrever esse teste e verifique se testar aquela classe de outra maneira (através de um teste de integração, por exemplo) não seria mais útil.

21 Comentários

  1. diegorv 10/12/2010 at 10:59 #

    Muito bom! 🙂

    Admito, que ainda não sou muito fluente em TDD/BDD, tenho a dificuldade de encontrar material bom falando sobre o que e como testar.

    Cheguei a conclusão que quando eu pergunto isso pra uma galera com mais experiência, as respostas são sempre iguais e acabam não respondendo nada.

    Estou trabalhando a pouco tempo com Ruby e Rails e adotei o seguinte padrão:

    Escrevo Features com Cucumber para testar Controllers e Views.
    Escrevo Specs pra testar coisas especificas de Models com Rspec.

    Queria bater um papo sobre isso, tenho duvidas ainda doq fazer e não fazer, não são duvidas técnicas 🙂

    Valeu, abraços 😉

  2. Diego Carrion 10/12/2010 at 11:22 #

    Acho que quando as pessoas perguntam se uma coisa deve ser testada ou não, elas ja começaram um pouco erradas, porque pensaram primeiro na implementação e depois no teste.

    Quando se faz o contrario, que eu acho uma tarefa difícil, me parece que esses testes sen nenhum valor somem, dado que pensamos no negocio e não no código.

  3. Tiago dos Santos 10/12/2010 at 13:55 #

    Sou novato com relação a testes unitários, então sempre fico com a pulga atras da orelha: será que é realmente importante testar isso? será que não estou esquecendo algum teste? Bom, mas o que tenho percebido é que conforme vamos testando e lendo sobre testes as dúvidas vão diminuindo.

    Acho que o caminho é esse, ir fazendo os testes e vendo como os mais experientes fazem se aprimorar.

  4. Dalton Barreto 10/12/2010 at 14:15 #

    Excelente texto! Já me flagrei várias vezes fazendo esse tipo de teste, onde estou testando “o que foi digitado” e não o resultado do código.

    Mas ainda tenho uma dúvida: E quando você precisa testar um código que depende de uma API externa? É correto realmente ter alguns testes que, por exemplo, dependam de ter uma rede disponível para passar?

    Qual a vantagem de mockar o retorno de um método dessa API? Seus testes vão passar *sempre*, afinal você pegou um resultado de exemplo da tal API. Mas se a API real começar a responder com um conteúdo diferente, seu teste continua passando mas sua aplicação já não funciona mais. O que vocês fazem nessas situações?

    Pense em um wrapper para o twitter por exemplo, se encaixa nesse caso.

    Abraços,

  5. Rafael Ponte 10/12/2010 at 15:18 #

    Muito bom o post.

    Confesso que quando iniciei com testes de unidade eu abusei muito de mocks, principalmente quando se tratava de JPA e Hibernate (mockando EntityManager, Query, Criteria etc), algo muito semelhante aos exemplos citados por vocês dois. Depois de muito trabalho após refatoração de código, algumas leituras e conversas com outros profissionais eu não faço mais isso e desaconselho este tipo de testes de unidade, deixando essa responsabilidade para um teste de integração.

    Hoje em dia fica bem mais fácil perceber que testes deste tipo são muito frágeis e não agregam o valor devido.

    Enfim, excelente post e gostei muito do assunto.

    Um abraço.

  6. Guilherme Silveira 10/12/2010 at 16:16 #

    @Diego o que acha de postar no tectura e discutirmos as abordagens?
    @Carrion com certeza e muito boa a maneira de resumir em poucas frases esse pensamento. A questão do “devo testar isso?” aparece em algumas situações que em geral não envolvem pensar em comportamento mas sim em implementação. Algumas outras situações seriam codigo legado e quando você testa, faz uma implementação e descobre que por causa de sua implementação, algo parece não testado (um baby step mais longo).

    @Tiago com certeza, com o passar do tempo, a experiência vai te dizendo melhor se você está atacando o dominio de testes que você esperava ou não. criar o teste antes não te garante que você criou todos os testes, mas a experiência vai te dar essa sensação. E alias, se a sensação está presente é pq sua unidade provavelmente é maior do que uma unidade deveria ser 🙂

    @Dalton, obrigado. Esse caso de testar o que foi digitado aparece até mesmo testando antes também (mas não a pergunta de se devo ou não testar, como o Carrion apontou perfeitamente), pois posso testar que invoquei o método com X parametros, e invoco, mas não tenho certeza que esse era o comportamento que eu desejava – justamente um teste sem valor.

  7. Mauricio Aniche 10/12/2010 at 16:25 #

    Rafael Ponte,

    Obrigado!

    Você tem toda razão. Você tem outros níveis de teste, e não só o de unidade; integração, carga, stress, aceitação, etc. Você pode (e deve!) usar cada um deles quando fizer sentido.

    Como você mesmo comentou, um teste de integração para uma classe que faz acesso ao banco de dados agrega muito mais valor!

    Mas é difícil: estamos tão acostumados com testes de unidade, que às vezes esquecemos dos outros! 🙂

  8. Bruno P. 10/12/2010 at 21:16 #

    Eu utilizo teste para verificar comportamento de meus objetos de negócio. Procuro stubs em testes unitários e mocks em testes de integração (em alguns casos não uso mocks nem stubs nos testes de integração). Pensando em DDD, o que é importante é o seu domínio, então foco neles. Um teste de sistema automatizado cobre as outras camadas.

  9. Tiago Peczenyj 11/12/2010 at 12:42 #

    Ola

    concordo um pouco com o @BrunoP e adiciono uma regrinha a mais: no caso de encontrar um bug crie um teste unitário que descubra o mesmo e, então, altere o programa. é util para evitar a re-introdução do bug.

    outra regra é rodar testes de “aceitação” constantemente, em algum horario noturno ou vespertino, para analisar consumo de memoria, performance, %cpu, etc. vc pode coletar metricas e até antever algum problema ao adicionar uma feature. se eu tivesse feito isso eu provavelmente não teria descoberto um memory leak em produção apenas

    felizmente o que o teste de unidade traz de vantagem adicionar é o design, especialmente OO 🙂

  10. camilo lopes 12/12/2010 at 17:16 #

    primeiramente parabenizar pelo post. Realmente excelente. E por acaso eu “discutia” com um colega de trabalho sobre o assunto de ganhar tempo ou perder com testes de unidades, ele disse que era perda de tempo, algo que um dia tb pensei qdo vi o assunto na primeira vez, achei estranho o lance de escrever primeiro o teste, mas fui adiante e com o tempo, vi que realmente o tempo que eu deixei de gastar para achar um bug, e saber se eu mudei algo em um lugar afetou outro, caiu pra mais que 50% e hj nao consigo fazer um sistema sem usar teste, é um ciclo vicioso, é “uma droga” das fortes :).
    A respeito de material tem um livro legal que gostei muito “pragmatic unit testing in Java with Junit” Andrew Huny/David Thomas
    Recomendo para quem está querendo iniciar, alem de explicar o pq do uso,e fazer comparacoes com os possiveis problemas que teriamos sem o uso de TDD, ele é pratico com exemplos para implementar que são excelente.

    flw.

  11. Guilherme Silveira 13/12/2010 at 11:13 #

    Oi Bruno! Com certeza, tentamos ir pelo mesmo caminho, sempre analisando caso a caso o que é um teste de integração – integrar com serviços web é uma coisa, integrar com um banco de dados é outra, devem ser testados do mesmo jeito? com o mesmo conjunto de ferramentas? (na minha opinião, depende do que será feito).

    Você mesmo citou a variabilidade – as vezes usa os mocks e as vezes não. Concordo.

    Tiago, melhor do que rodar os de aceitação periodicamente é se conseguir rodá-los continuamente, a cada commit, fica mais fácil achar o commit que quebrou um comportamento. Claro que isso depende da velocidade dos testes de aceitação e do impacto deles no processo de build.

    Abraço e ótimos comentários, o post só fica mais rico com isso!

  12. Francislon Silva 13/12/2010 at 18:36 #

    Muito bom o post pessoal. Só gostaria de fazer uma ressalva boba. No Início do post quando você fala de constantes e declara: public static int VISA = 123;
    Ali você está declarando apenas uma variável estática e não uma constante 😉
    Faz esse consertinho ai que o post fica perfeito.

  13. Paulo Silveira 13/12/2010 at 18:48 #

    @Francislon, acrescentamos o final que estava faltando no codigo para ser uma constante.

  14. Tiago Peczenyj 14/12/2010 at 09:36 #

    @Guilherme

    para rodar os testes de aceitação com alguma velocidade vcs ja usaram o Selenium Grid, não? ele continua sendo a melhor opção?

  15. Guilherme Silveira 14/12/2010 at 14:26 #

    @Tiago

    Isso mesmo. Tem um post no blog sobre o uso e problemas do selenium grid. Hoje em dia preferimos distribuir os testes de integração muito lentos na unha dentro de nossos scripts de ant (ou similares), também estamos encontrando caminhos alternativos a muitos testes pesados end to end.
    Vale uma discussão no tectura sobre o assunto?

    Abraçø!

  16. Washington Botelho 15/12/2010 at 23:35 #

    Esse post fala exatamente o que eu disse para alguns colégas a um bom tempo atrás.

    O vício de se criar mocks e testar o que esta escrito na aplicação é muito grande quando se usa JPA/Hibernate. Creio que no início é por falta de experiência mesmo, até mesmo porque, por exemplo, um jMock da vida facilita demais você fazer esses tipos de testes e eu passei por isso até ver que não estava agregando valor e eu estava “programando” a mesma coisa em dois lugares.

    Uma dificuldade que tive de teste foi usando JSF com Open Session In View e outras coisas mais. O teste ficava muito acoplado a framework e me dificultava muito fazê-los.

    O que vocês acham sobre o BDD no início da aplicação? Digo isso porque acaba se tornando um trabalho meio chatinho por conta das constantes mudanças na interface, além dos problemas assíncronos e as vezes estruturais (componentes) que não facilitam.

    Muito bom o post.

  17. Hugo Luiz 18/09/2013 at 16:46 #

    Está muito bem escrito o post, parabéns. Tenho certeza que vai ajudar para minha apresentação.

  18. Mauricio Aniche 18/09/2013 at 17:07 #

    Oi Hugo,

    Que bom que gostou! Depois dê uma olhada também no meu livro sobre TDD: tddnomundoreal.com.br. Ele pode ajudar!

    Um abraço!

  19. Rogerio J. Gentil 03/01/2015 at 23:19 #

    Já estou lendo o seu livro @mauricioaniche e este post me ajudou ainda mais a entender o paradigma do TDD e outros tipos de testes.

  20. Lucas Martins 22/02/2017 at 14:25 #

    Muito esclarecedor esse post, bem intuitivo e prático. Obrigado!

Deixe uma resposta