Perdendo ou ganhando tempo com testes de unidade

Perdendo ou ganhando tempo com testes de unidade
maniche
maniche

Compartilhe

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; } 
Banner da Escola de Inovação e Gestão: Matricula-se na escola de Inovação e Gestão. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

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.

Veja outros artigos sobre Inovação & Gestão