Os 8 erros mais comuns ao escrever testes de unidade (atenção especial para os 2 últimos)

Introdução

Os testes de unidade são o tipo de testes mais comuns – e por que não dizer os mais importantes – dentro de uma aplicação. A maior parte da quantidade de testes que escrevemos são testes unitários (veja a figura abaixo). Portanto eles também são os mais passíveis de serem acometidos com erros e falhas.

Pirâmide ideal de testes
Pirâmide ideal de testes. Observe que a maior parte dos testes – base da pirâmide – são testes de unidade.

Este post é resultado de uma pesquisa informal que fiz (tanto em blogs, artigos e livros) e que juntei erros mais comuns cometidos pelos desenvolvedores ao se escrever testes de unidade. Esta lista não tem por objetivo ser completa. Meu objetivo aqui é apenas mostrar aqueles que julguei mais importantes seja pela quantidade de referências encontradas seja por experiência própria ou de outros colegas desenvolvedores.

Se você quer escrever testes de unidade melhores e, consequentemente, melhorar a qualidade interna e externa do seu código, leia esse post e veja os erros que você deve evitar ao escrever testes de unidade. São eles:

  1. Querer atingir cobertura de testes 100%
  2. Não executar os testes de unidade frequentemente
  3. Escrever testes que dependem um dos outros
  4. Escrever métodos de testes com nome inadequado
  5. Fazer múltiplos asserts no mesmo método
  6. Fazer muitos mocks nos seus testes
  7. Não tratar o código de teste como código de produção
  8. Não ouvir o código de testes

Portanto, vamos detalhá-los agora.

#Erro 1: Querer atingir cobertura de testes 100%

Um erro comum em quem escreve testes de unidade é almejar ter cobertura de testes 100%. A cobertura de testes mede o quanto de um código é executado quando rodamos uma suite de teste.

Precisamos esclarecer alguns pontos quando falamos em cobertura de testes. Segundo Martin Fowler, as pessoas costumam não entender o objetivo da métrica cobertura de testes: ela é uma ferramenta útil para encontrarmos partes do código que não foram testadas. Já como um valor numérico (60%, 70% ou 100%), ela possui pouco valor em dizer o quão bons os testes escritos são.

O que importa não é cobrir 100% do código com seus testes, mas sim garantir que eles são bons o suficiente. Eles devem garantir:

  1. Que raramente temos bugs em produção – indicador de que os testes estão capturando boa parte dos problemas.
  2. Que você não tenha medo de modificar um código porque acha que sua mudança pode introduzir um novo bug – indicador que seus testes são tão confiáveis que você pode fazer refactoring no código sem problemas.

Não se apegue muito aos números. Claro que cobertura muito baixa, usualmente abaixo de 50%, pode ser um indicador de problema – você pode não estar testando suficientemente. Mas, ao mesmo tempo, cobertura muito alta pode indicar que você está perdendo tempo escrevendo testes para coisas que não importa e deixando de se concentrar no que realmente é relevante (lembra de Pareto: 80%/20%??).

Por exemplo, veja os 2 métodos no código Java abaixo.


  public void setPreco(BigDecimal preco) {
     this.preco = preco;
  }

  public void incluirPalestra(Palestra palestra) {
     repository.incluir(palestra);
  }

Escrever testes para estes métodos não acrescenta nada. No máximo, como costumo dizer, você está testando a digitação do código! O primeiro método é um setter muito simples: atribui um valor a uma variável. O segundo apenas delega a ação para a camada de persistência. São métodos muito pouco prováveis de dar algum erro.

Portanto, a cobertura 100% não te diz nada sobre a qualidade dos seus testes. Duvide de objetivos do tipo “não podemos ir para produção se não tivermos X% de cobertura de testes”. O que devemos almejar são testes de qualidade: testes que diminuam a quantidade de erros em produção e que proporcionem confiança de mexer no código-fonte do produto sem ter medo de adicionar novos bugs.

Em vez de se preocupar com os números, utilize a cobertura de testes como uma ferramenta que lhe permita descobrir caminhos críticos de sua aplicação que estão sem testes de unidade. Sobre esse assunto, sugiro a leitura do post do Rafael Chies sobre a diferença entre cobertura 100% e código 100% testado. Vale a pena.

#Erro 2: Não executar os testes de unidade frequentemente

Ok. Você decidiu escrever testes de unidade para sua aplicação. Mas com que frequência você executa esses testes? Apenas na hora de gerar o build para o ambiente de homologação e/ou produção? Os seus testes só são executados uma vez por dia em nightly builds? Se você respondeu sim para alguma dessas perguntas, temos problemas.

Os testes de unidade devem ser executados frequentemente. Quando digo frequentemente, quero dizer o tempo todo! Cada commit feito pelo desenvolvedor no sistema controlador de versões deve disparar a execução dos testes de unidade. Isso porque os testes de unidade, dentre outras finalidades, servem para fornecer feedback contínuo aos desenvolvedores. Com uma suite de teste robusta que executa frequentemente, o desenvolvedor é sempre avisado caso o seu código tenha quebrado alguma outra funcionalidade além daquela que ele está trabalhando – a famosa regressão.

Os desenvolvedores de software estão continuamente agregando valor ao produto, escrevendo códigos para adicionar novas funcionalidades ou corrigir erros existentes. Cada uma dessas agregações de valor tem o potencial de ser um novo release do produto; mas nós, desenvolvedores, não temos certeza de que tudo está funcionando até que tudo seja integrado.

Portanto, para garantir que tudo funcione, o desenvolvedor deve executar os testes em seu ambiente local antes do commit e depois ter seu código integrado ao restante do sistema e ter seus testes – e dos outros membros da equipe – executados após o seu commit. Nesta última etapa, o costume é fazer uso de servidores de integração contínua como o Jenkins, Travis, Go etc. Assim, em vez de executarmos os testes manualmente, passamos a executá-los continuamente de forma automatizada.

Continuous_Delivery_process_diagram
Processo de entrega contínua de software. Os testes de unidade precisam rodar frequentemente para esse ciclo funcionar.

A ideia, portanto, é fazer com que os testes de unidade nos ajudem a capturar o problema o mais cedo possível, quando é mais barato consertá-los. Execute-os continuamente utilizando um servidor de integração contínua.

De nada adianta ter testes que são executados raramente. Eles se tornam inúteis.

#Erro 3: Escrever testes que dependem um dos outros

Quando escrevemos testes, estamos pensando em testar um cenário específico. Este cenário tem sua própria realidade: os dados a serem preparados estão relacionados ao cenário de teste; as verificações a serem feitas acerca do resultado da execução também são próprias do cenário sendo testado. Portanto, cada método de teste constitui uma realidade particular, independente.

O paralelo que gosto de fazer, nestes casos, é comparar os testes automatizados com os testes feitos manualmente. Quando vamos fazer um teste manual, devemos partir de um estado inicial limpo: os dados e entradas a serem utilizadas no teste devem ser criados para aquele teste em específico. Quando formos fazer outro teste manual, não é uma boa prática reutilizar dados de testes anteriores, pois, o novo teste pode ficar viciado – o seu resultado pode vir a ser afetado pelo uso de dados de testes anteriores. Tanto que uma prática comum, nesses casos, é após o teste ser feito, é remover todos os dados que foram utilizados naquele teste.

O mesmo deve se aplicar aos testes automatizados. Cada cenário deve ser completamente isolado do outro, de forma que nossos testes não se tornem viciados. Além disso, quando escrevemos testes que são dependentes de outros, quando um deles falha, fica mais difícil descobrir o motivo: ele falhou porque realmente o código a ser testado tem um erro ou ele falhou porque um método de teste anterior, do qual o nosso dependia, também falhou?

Podemos compartilhar a inicialização de dados entre os testes (para isso temos, por exemplo, as fixtures do JUnit). Mas os testes de unidade devem independentes um dos outros!

#Erro 4: Escrever métodos de testes com nome inadequado

no_name_exit_colorado_by_roger_wendell_10-04-2010

O nome do método de teste unitário, assim como qualquer outro método em seu projeto, deve expressar sua intenção. Deve ser possível saber o que o método faz – ou nesse caso, testa – apenas lendo o nome do método.

Os testes de unidade servem como uma documentação para desenvolvedores. Desta forma, a legibilidade é uma característica fundamental de testes de unidade e deve ser buscada constantemente. A legibilidade impacta na manutenção do código. Se a legibilidade for prejudicada, a manutenção do código torna-se mais difícil.

Os testes, então, mais do que verificar o comportamento do código que está sendo testado devem expressar o seu comportamento e intenção claramente. Se o desenvolvedor tem que parar muito tempo para entender o que o código de teste faz, ele está tendo sua produtividade diminuída.

O nome do teste deve ser o primeiro sinal para que um desenvolvedor entenda o que está sendo testado e como o objeto alvo do teste supostamente se comporta.

A abordagem que gosto de seguir é a apresentada no livro Growing Object-Oriented Software, Guided by Tests, chamada convenção TestDox. Nesta abordagem o nome de cada teste representa uma sentença que deve dizer o que o objeto sendo testado deve fazer. Os nomes devem explicar não apenas o cenário sendo testado mas, também, devem explicitar o resultado esperado.

Por exemplo. Imagine que vamos testar uma classe TransferenciaBancariaService. Esta classe:

  • Deve fazer a transferência entre duas contas bancárias, debitando o valor de uma e creditar este valor na outra conta.
  • Deve lançar uma exceção caso a conta a ser debitada não possua saldo suficiente para o débito.

Podemos criar métodos de testes para esta classe, segundo a convenção TestDox, com os seguintes nomes (utilizando o JUnit):

 @Test
 public void transfereValorEntreContasBancariasDebitaContaCreditaOutraConta() {}

 @Test
 public void lancaExcecaoQuandoContaDebitoNaoTemSaldoSuficienteParaTransferencia() {}

Observe esses nomes. Eles expressam claramente o cenário sendo testado e como o objeto a ser testado deve se comportar. O desenvolvedor lê o nome desses métodos e sabe exatamente o que eles fazem. Os nomes são longos? Sim. E isso não é problema. Eles devem ser tão longos quanto necessário; a ideia é fazer com o que o nome dos métodos expressem exatamente sua intenção.

Quando penso em legibilidade de código – e os nomes de métodos estão aí incluídos – sempre gosto de lembrar do que diz Martin Fowler:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

Pense nisso.

#Erro 5: Fazer múltiplos asserts no mesmo método

A estrutura de um teste tem 3 passos: preparar os dados; chamar o método a ser testado; e verificar o resultado da execução (asserções). Vamos falar agora desta terceira parte, a verificação.

Ter múltiplos asserts em um método de teste geralmente é indicativo de problemas.

Primeiramente, vamos nos lembrar do erro anterior, onde falamos sobre o nome de métodos. A conclusão a que chegamos é que o nome deve expressar claramente sua intenção, deixando claro o que deve ser testado e qual o resultado esperado. Portanto, quando escrevemos um teste, esperamos por um resultado. Ter múltiplas verificações pode significar que mais de um resultado está sendo retornado, o que traz consigo pelo menos dois problemas:

  1. o nome do método de teste não está condizendo com o que ele faz (não expressa sua intenção);
  2. e o código alvo do teste pode estar com mais responsabilidades do que devia – o código pode estar complexo e deve ser refatorado.

Um outro problema com múltiplas verificações é determinar a causa da falha do teste. Um teste deveria falhar por um único motivo, indicando que o comportamento sendo testado não foi o esperado. Com múltiplos asserts em um método, é mais difícil determinar o que causou a falha do teste. Ou seja, as coisas ficam muito mais difíceis de serem corrigidas.

Se você fez múltiplas verificações em um mesmo método de teste, fortes são os indícios de que você deveria ter mais de um método de teste: um para cada cenário verificado por cada asserção.

Para finalizar, mostro abaixo uma exceção a essa regra, onde múltiplos asserts são aceitáveis.

 @Test
 public void extratoMensalTrazRegistros() {
   // preparação
   ...
   // execução
   Extrato extratoMensal = extratoService.consultaExtrato(mes, ano);
   // verificações
   assertNotNull(extratoMensal.getRegistros());
   assertThat(extratoMensal.getRegistros(), is(not(empty())));
 }
 

Veja que o código anterior faz mais de uma verificação nos registros do extrato. No entanto, observe que elas estão relacionadas ao mesmo conceito: verificar que o extrato mensal possui registros. Em casos como esse, é perfeitamente aceitável ter múltiplos asserts em um mesmo método de teste.

Quer saber mais sobre asserções? Leia aqui o post sobre como escrever asserções mais legíveis!

#Erro 6: Fazer muitos mocks nos seus testes

mock é uma ferramenta muito útil quando estamos escrevendo testes de unidade. Utilizamos eles em nossos testes para simular o acesso a recursos externos. Vejam o código do método abaixo.

 public void transfereValor(ContaBancaria origem, ContaBancaria destino, BigDecimal valor) {
   origem.debita(valor);
   destino.credita(valor);
   notificador.enviaEmail(origem.titular(), "Transferência executada com sucesso!");
 }

O código é de uma transferência de valor entre duas contas. Ao final desse método, é enviado um e-mail para o titular da conta origem informando do sucesso da transferência.

Se formos escrever um teste de unidade para esse método vamos ter um complicador: o notificador vai precisar ter acesso à rede para fazer o envio de e-mail. Para o nosso teste isso não faz sentido pois:

  1. O código de teste vai ficar mais complexo. Ter que disponibilizar uma infra-estrutura com acesso à rede de dados torna a preparação do teste mais complicada.
  2. A execução do teste será mais lenta. Preparar o acesso à rede e esperar o envio do e-mail torna a execução mais lenta. Lembre-se: testes de unidade devem fornecer feedback rápido!
  3. O envio de e-mail não é o alvo do teste. Queremos testar a transferência de valor em isolamento; o envio de e-mail não é o alvo do nosso teste.

Para casos como esse, em que fazemos o acesso a recursos externos, criamos um objeto mock. Neste caso, criaríamos em nosso teste um mock do notificador que simularia o envio do e-mail sem precisar acessar a rede. Isso permitiria o teste isolado da regra de transferência de forma muito mais rápida.

Agora, vejam os dois trechos de código a seguir.

 @Test
 public void debitaContaOrigemQuantoTransfereValorEntreContaBancarias() {
   ContaBancaria origem = new ContaBancaria("CB-001", BigDecimal.valueOf(10000.00));
   ContaBancaria destino = new ContaBancaria("CB-002", BigDecimal.valueOf(7500.00));

   TransferenciaService servicoTransferencia = new TransferenciaService();

   when(mockVerificadorCredito.isCreditoDisponivel()).thenReturn(true));
   doNothing().when(mockNotificador.enviaEmail(origem.getTitular(), anyString()));
   doNothing().when(mockDaoConta.salvar(any(Conta.class))).doNothing();

   servicoTransferencia.transfere(origem, destino, BigDecimal.valueOf(1500.00));
   assertEquals(BigDecimal.valueOf(9000.00), destino.getSaldo());
 }
 @Test
 public void debitaContaOrigemQuantoTransfereValorEntreContaBancarias() {
   ContaBancaria origem = new ContaBancaria("CB-001", BigDecimal.valueOf(10000.00));
   ContaBancaria destino = new ContaBancaria("CB-002", BigDecimal.valueOf(7500.00));

   TransferenciaService servicoTransferencia = new TransferenciaService();

   servicoTransferencia.transfere(origem, destino, BigDecimal.valueOf(1500.00));
   assertEquals(BigDecimal.valueOf(9000.00), origem.getSaldo());
 }

O segundo trecho de código é bem mais claro e muito mais fácil de entender. O uso dos mocks no primeiro trecho tornou o código mais complicado e menos legível. Quando abusamos de mocks como nesse caso, temos alguns problemas:

  1. Os testes ficam mais difíceis de se entender. Em vez de simplesmente preparar os dados e chamar o método sendo testado, estamos colocando código adicional para ensinar aos mocks como eles devem se comportar. Isso é um claro desvio da intenção do nosso método: testar algum comportamento. O código fica mais difícil de ler e entender.
  2. Os testes não fornecem a garantia de que seu código funciona adequadamente. Quando fazemos uso de mocks  dizendo a eles como se comportar em determinadas situações, os nossos testes garantem apenas que nosso código funciona naquelas condições simuladas pelos mocks. Pode ser que, com o passar do tempo, nossa aplicação comece a dar erro por que o comportamento de alguma das dependências daquele método mudou. No entanto, o código de teste continua sem falhar, ignorando esse erro, pois o mock foi ensinado a ter um determinado comportamento que não condiz mais com a realidade. Assim, nosso teste perdeu a utilidade.
  3. Violações de encapsulamento e abstração.  Quando ensinamos aos mocks como se comportar, estamos vazando detalhes de implementação do nosso código para os nossos testes. Se modificarmos o código de nossa aplicação, vamos precisar alterar o código de teste para refletir essas mudanças. Os testes não deveriam conhecer os aspectos internos do nosso código; eles devem testar o comportamento da interface pública do código.

Como comecei escrevendo esse tópico, os mocks são muito úteis. No entanto, evite seu uso exagerado. Se em seu código de testes você estiver criando mocks para mais de uma ou duas classes, ligue o sinal de alerta: tanto seu código de teste quanto o código de produção podem estar ficando muito complexos!

Quer aprender a criar mocks com o Mockito? Leia agora mesmo o post sobre o uso do Mockito em testes de unidade no Java.

#Erro 7: Não tratar o código de teste como código de produção

No post em que escrevi sobre Selenium e Page Objects comentei um pouco sobre isso, mas vou reforçar isso um pouco mais agora.

Costumeiramente o nosso código de produção é o que merece maior atenção. Em uma aplicação que utiliza uma linguagem com paradigma orientado a objetos, com vistas a facilitarmos a manutenção e evolução do nosso software, procuramos aplicar técnicas que facilitem o alcance desse objetivo: fazemos uso de polimorfismo, programação voltada para interface, uso de padrões de projeto, divisão de responsabilidade etc.

Até avaliação de estilo e avaliação estática do código-fonte fazemos, utilizando ferramentas como o FindBugs, CheckStyle, Sonar, dentre outras, para descobrimos problemas e avaliarmos métricas de qualidade do código que escrevemos.

Mas e o nosso código de teste unitário? Como fica? Ele também não merece esses cuidados especiais que temos com o código de produção? A resposta é sim. Ele merece todos esses cuidados. Negligenciar esses aspectos que facilitam manutenção e evolução do código do teste é um erro. O código de teste é um dos principais responsáveis por garantir a qualidade do código de produção: tanto em termos de erros existentes quanto na sua própria qualidade intrínseca (veja o próximo erro). Logo, temos que dar ao nosso código de teste a mesma atenção que damos ao nosso código de produção.

E por que isso? Porque o nosso código de teste, invariavelmente como e todo e qualquer código, vai precisar ser mantido e evoluído. Quando o nosso código de produção quebrar, provavelmente vamos ter que verificar os testes: lê-los e modificá-los para contemplar essa nova situação de erro.

Outra situação comum de modificação do código de teste é quando fazemos refactoring do código de aplicação. O refactoring é uma prática comum na vida do desenvolvedor de software. Constantemente precisamos refatorar nosso código com o objetivo de torná-lo melhor – é a famosa regra do escoteiro: deixar as coisas melhor do que encontramos. Refactoring envolve reescrever o código, modificando sua estrutura sem modificar o comportamento dele. O ciclo de refactoring seguro envolve testes de unidade:

  • Antes de modificar o código, devemos rodar os testes para garantir que tudo está ok, sem erros;
  • Fazemos o refactoring do código;
  • Rodamos os testes novamente para garantir que nossa modificação não quebrou o que funcionava – isso não deve acontecer, pois o refactoring modifica a estrutura sem mexer no comportamento/resultado do código.

Portanto, o refactoring pode implicar em modificação dos testes.

E como cuidamos do nosso código de testes de forma a torná-lo de fácil evolução e manutenção? Simples. Utilizando as mesmas técnicas que aplicamos em nosso código de produção. Destaco aqui alguns pontos que julgo importante de usar em testes de unidade com vistas ao alcance desse objetivo:

  1. Evitar a repetição de código. O famoso DRY (Don’t Repeat Yourself). Evitar duplicidade de código é chave fundamental na evolução/manutenção de código; quando precisamos mexer em código que se repete, temos que fazê-lo em diversos pontos. O mesmo se aplica aos códigos de testes. Evite criar código que se repete. O JUnit oferece fixtures (na versão 4, disponíveis por meio das anotações @Before, @After, @BeforeClass e @AfterClass) que possibilitam colocar códigos comuns entre os métodos de testes em um só lugar. Além disso, use e abuse de classes/métodos utilitários que possam ser compartilhados pelo código de teste.
  2. Evitar acoplamento com APIs externas. Evite acoplar com APIs que não fazem parte de sua aplicação. Tente ao máximo escrever código de teste na linguagem de sua aplicação. Assim, a abstração e o encapsulamento são mantidos. Caso, futuramente, você troque a implementação de sua regra negocial, substituindo uma biblioteca, por exemplo, não tê-la em seu código de teste é um ponto a menos de mudança. O teste deve se preocupar com a interface pública do código sendo testado. Ele não deve conhecer aspectos internos.
  3. Legibilidade e tamanho do código. Procure escrever testes legíveis, nos quais os desenvolvedores consigam entender o que é feito apenas lendo o código, sem precisar ter que verificar detalhes de implementação. A escolha do nome do teste (vide Erro 4) faz parte dessa regra. Procure também escrever métodos de testes não muito grandes (com mais de 20 linhas). Embora isso não seja totalmente verdade, códigos menores costumam ser mais fáceis de serem compreendidos. Se o código está muito grande, use técnicas de refactoring: extrair parte do código para outros métodos ou outras classes costuma solucionar esse tipo de problema.
  4. Utilize padrões de projetos de criação. Nos métodos de testes, como parte da etapa de preparação dos dados antes da execução do teste, é comum criarmos objetos. Por vezes, essa criação torna-se complexa. Passe a utilizar padrões como builders e fábricas em seus códigos de teste para reduzir a complexidade e delegar corretamente responsabilidade de criação de objetos. Sugiro a leitura desse post do Robson Castilho sobre o uso de test data builders na criação de objetos complexos para testes. Sugiro também que deem uma olhada no framework make-it-easy, que permite a criação de Test Data Builders em Java.

#Erro 8: Não ouvir o código de testes

O código de teste unitário costuma dizer muito sobre o código de sua aplicação. O teste unitário não está ligado apenas a detecção de erros de sua aplicação. Ele também se relaciona com o design da mesma: o feedback nesse caso é sobre as qualidades internas do código: acoplamento e coesão entre classes, dependências explícitas, encapsulamento e abstração, enfim, qualidades que facilitam a evolução e manutenção do código.

Um código de teste complexo costuma ser reflexo de um código complexo, difícil de se manter e evoluir. O primeiro sinal de que o código de sua aplicação não está bom é quando se torna difícil escrever código de teste para ela.

Quando isso acontece, os testes estão nos dando a oportunidade de melhorarmos o nosso código de produção.

A seguir apresento alguns sinais que os testes nos dão de que o código da aplicação precisa ser melhorado.

  1. Criação de muitos mocks. Já falei sobre isso no Erro 6. Se para testar um código você está precisando fazer mocks de muitos objetos, os testes estão te alertando sobre acoplamento e dependências. O seu código pode estar muito acoplado a dependências externas – e alto acoplamento não costuma cheirar bem. Além disso, a abstração do código está ruim – você está precisando conhecer demais o código a ser testado; na verdade, você só deveria se preocupar com a interface pública do código: seus argumentos e o comportamento resultante da execução.
  2. Fazendo múltiplas asserções. Se você faz diversas verificações no resultado da execução do teste, o seu teste está te alertando sobre as responsabilidades do seu código. Talvez ele esteja fazendo coisas demais e violando o princípio de responsabilidade única. Se isso for verdade, é hora de quebrar essas responsabilidades e torná-lo mais simples.
  3. Criação do objeto a ser testado é complexa. Às vezes, quando vamos criar o objeto a ser testado, nos deparamos tendo que criar outros objetos para criar o primeiro. O construtor fica sendo uma lista longa, com diversos argumentos. Aqui os testes estão nos avisando sobre acoplamento e divisão de responsabilidade. Talvez seja a hora de agrupar parte desses argumentos em um só conceito e substituí-lo por um objeto. Talvez, alguns desses argumentos tenha uso limitado a um ou poucos métodos e possa ter seu escopo reduzido para uso apenas nesses métodos. Talvez a criação do objeto possa ser delegado para uma fábrica. Enfim, os testes estão nos possibilitando um momento de melhoria do código.
  4. Objeto muito grande e confuso. Imagine que você está implementando uma funcionalidade e se deparou com um objeto que tem um método que pode lhe servir. Ao verificar a suite de teste desse objeto, você se assusta com a quantidade de casos de teste existentes. Ao olhar o código do classe objeto, o susto é maior ainda: o objeto é um monstro! Os testes estão te dizendo que essa classe está com responsabilidades demais, fazendo coisas demais, muitas sem ligações uma com as outras – baixa coesão. É o momento de quebrar essa classe em outras, tornando-a mais coesa e mais simples.
  5. Você está lidando diretamente com implementações em seus códigos de teste. Veja essa situação. Você escreveu um código de teste e pegou o retorno da execução de um método para fazer as verificações. O valor retornado é uma coleção. Daí você verifica o tamanho da coleção, itera a coleção para verificar os valores de seus elementos. Pare por aí. O código de teste está te avisando que você está muito dependente da implementação do método. A abstração e encapsulamento da informação está ruim. Há vazamentos aqui. Se amanhã for preciso alterar o retorno de uma coleção para um array, por exemplo, todos os lugares que chamam esse método – incluindo o código de teste – precisarão ser alterados. É hora de melhorar isso. Procurar formas melhor de encapsular essa informação retornada, tornando os clientes desse método menos conhecedores de detalhes da implementação.

A lista acima não é exaustiva. Eu quis apenas deixar clara a importância de não ignorar os alertas que nosso código de teste fornece. Tenha em mente: as qualidades que tornam fácil a escrita de testes também tornam o nosso código mais fácil de ser mantido e evoluído.

Conclusão

Esta lista ilustra erros comuns cometidos ao se escrever testes de unidade.

É uma lista que deve estar sempre em nossa cabeça quando vamos escrever testes de forma que possamos evitá-los e produzir códigos de teste melhores e, consequentemente, possibilitar a melhoria da qualidade interna e externa da nossa aplicação.

Sugiro atenção especial aos 2 últimos erros: evitá-los possibilita a melhoria contínua do código de nossa aplicação, facilitando a sua manutenção e evolução.

  • Rogerio J. Gentil

    Excelentes dicas. É sempre bom ter em mente essas questões.

    • É isso mesmo @rogeriojgentil:disqus. São pontos que penso que sempre devem ser considerados no momento de escrita de testes unitários.

  • Alexandre Kiyoshi Missawa

    Muito engraçado que o primeiro “erro” apresentado simplesmente vai de cara com uma das normas da DO-178B e DO-178C, da FAA, podendo nem ser aprovado para ser um software a ser utilizado em aviação. Não é nenhum erro se preocupar em atingir 100%, o erro é se preocupar mais em atingir 100% do que realizar testes efetivos.

  • amissawa

    Muito engraçado que o “Erro #1” apresentado simplesmente vai de cara com uma das normas da DO-178B e DO-178C, da FAA, podendo nem ser aprovado para ser um software a ser utilizado em aviação. Além disso faz parte da ISO 26262 para automóveis. Claro que atingir 100% é menos importante do que realizar testes efetivos, mas não é nenhum erro se preocupar em atingir 100%.