Facilitando a manutenção dos testes ao diminuir o acoplamento com o código
É fácil entender por que devemos diminuir o acoplamento entre classes: a alteração em um ponto do sistema pode propagar a necessidade de mudanças em outros. Dependendo do acoplamento, uma simples alteração exige um esforço enorme. Em alguns casos as mudanças não são feitas, e a funcionalidade é simplesmente descartada, devido a esse alto custo de manutenção deixado pelo acoplamento . Uma das tentativas para diminui-lo é tentar sempre programar voltado para interfaces e não para uma implementação e TDD pode ser utilizado para atingir esse objetivo.
Mas é mais difícil perceber que o acoplamento entre classes possa existir até mesmo entre o código de teste e o código de produção. Isso pode também fazer com que alterações também sejam caras.
Testes de integração, que visam verificar o comportamento de várias unidades juntas dentro de um sistema, dão um excelente feedback sobre a corretude do programa, mas geram um acoplamento muito forte entre o teste e o código. Observe o teste de integração abaixo:
public void deveCompararOValorDaAcaoDeDuasEmpresasDiferentes() { // usando um HttpClient simples String uri = "http://servico_falso_da_bovespa_para_testes"; HttpClient client = new HttpClient(uri); BuscadorDePrecos buscador = new BuscadorDePrecosNaBovespa(client); Comparador compara = new Comparador(buscador); double diferenca = compara.acoesDa("petrobras", "vale"); assertEquals(1.0, diferenca, 0.001); }
O teste acima verifica se a classe Comparador
calcula a diferença entre duas ações. Para isso, ela usa a classe BuscadorDePrecosNaBovespa
que, por sua vez, acessa um cliente qualquer de Http (aqui ilustrado como HttpClient
) para fazer as requisições.
Esse teste entrega feedback sobre a corretude do programa; batendo contra um servidor parecido com o da Bovespa, recebendo os valores e executando as comparações devidas. Se o programador deseja alterar a implementação do BuscadorDePrecosNaBovespa
, removendo o HttpClient
para fazer uso de uma outra ferramenta, como o Restfulie, além de quebrar os testes da classe BuscadorDePrecosNaBovespa
(que já era esperado), ainda quebrariam os do Comparador
. Uma mudança simples, que deveria acarretar em consequências apenas nos testes específicos dessa classe, afeta diferentes classes e testes desse sistema; aumentando o custo de manutenção da bateria de testes, um tradeoff que pode ou não fazer sentido em seu projeto.
Perceba que a classe Comparador
não está interessada em como a classe BuscadorDePrecosNaBovespa
faz seu trabalho; ela apenas espera que alguém busque esse valor. Se a classe Comparador
fosse testada isoladamente, tudo que ela precisaria é receber “alguém” que retorne o valor de uma determinada ação. Ou seja:
public interface BuscadorDePrecos { Acao pega(String empresa); } public class ComparadorTest { @Test public void deveCompararOValorDaAcaoDeDuasEmpresasDiferentes() { BuscadorDePrecos buscador = mock(BuscadorDePrecos.class); when(buscador.pega("petrobras")).thenReturn(new Acao("petrobras", 101.0); when(buscador.pega("vale")).thenReturn(new Acao("vale", 100.0); Comparador compara = new Comparador(buscador); double diferenca = compara.acoesDa("petrobras", "vale"); assertEquals(1.0, diferenca, 0.001); } }
A classe Comparador
, testada isoladamente, ainda depende de um BuscadorDePrecos
, um objeto que saiba como buscar o preço de ações. Mas agora o teste passa para ela um dublê, uma classe que finge ser a outra. O teste desconhece a existência de uma implementação concreta dessa interface.
A classe BuscadorDePrecosNaBovespa
(que implementa essa interface) pode ser modificada, sem propagar o custo de manutenção para além da sua própria classe de testes.
Programar para interfaces não só diminui o acoplamento entre as classes de produção, mas também entre seu código de teste e de produção. Devemos buscar sempre o baixo acoplamento, para diminuir os custos de manutenção, não importando onde ele esteja presente.
Testes de integração são fundamentais para validar a corretude do sistema, mas podem apresentar uma barreira para sua evolução. Os desenvolvedores devem pesar a abordagem de testes para garantir o máximo de qualidade interna e externa, mantendo um custo aceitável para o desenvolvimento e evolução a longo prazo.
Programação, Mobile, Front-end, Design & UX, Infraestrutura e Business
Ola,
O Buscador de preços em dado momento sera inicializado com os valores corretos do client REST, por exemplo, e a gerência desses objetos em produção geralmente é feita via DI, certo?
Bom, como garantir que a DI esta passando os valores corretos em produção? Eu preciso de um teste de integração ou posso simplesmente inspecionar se o setup foi adequado?
Olá Tiago,
Se não entendi errado, se você estiver utilizando um IoC Container como Spring por exemplo, você pode fazer seus testes de integração em cima do container (Spring-testing) e ter certeza que todos (ou a maioria) dos teus componentes estão sendo injetados corretamente como esperado em produção.
Oi Tiago,
Realmente, fazer um teste de unidade não remove a necessidade de um teste de integração para garantir a integração (no texto isso aparece como: “fundamentais para validar a corretude”). Mas não fazer o teste de unidade não dá outro feedback. Por isso a percepção que temos é que os dois são necessários, sendo importante encontrar a quantia exata de integração que deseja manter – principalmente em casos end-to-end com interface gráfica.
Abraço
Parabéns pelo post.
Uma observação: talvez uma falta de atenção:
when(buscador.busca(“petrobras”)).thenReturn(new Acao(“petrobras”, 101.0);
when(buscador.busca(“petrobras”)).thenReturn(new Acao(“vale”, 100.0);
O segundo deveria ser buscador.busca(“vale”), não?
Oi Renato,
Perfeito, já corrigi no post!
Abraços,
Mauricio
Concordo com os comentários. Testes unitários e de integração têm objetivos diferentes, e, por isso, um deles pode não encontrar determinado erro, como no exemplo citado (um teste unitário não encontraria um erro na injeção de dependências).
Por isso, o ideal é que tenhamos tanto testes unitários quanto de integração.
O texto menciona que o teste no exemplo é de integração. Como Comparador recebe em seu construtor um BuscadorDePrecos, como seria um teste de unidade para Comparador?
Estou viajando? Porque se a dependencia está no construtor, como fazer um teste de unidade, já que teremos que injetar a dependencia (mesmo que seja um mock)?
Abraços
Oi Marcos,
O último exemplo de código é um teste do Comparador em isolado (como unidade). Ele simula o comportamento de um BuscadorDePreco pois nesse teste o objetivo é entender como o comparador funciona e não somente seu resultado. A dependência injetada pelo instrutor, funcionando como um dublê, permite que testemos seu comportamento em isolado do comportamento do objeto vizinho.
Abraço!
Olá Guilherme,
Neste caso então, o teste que era de integração passou a ser de unidade, correto? E essa era mesmo a intenção, pois o desenvolvedor estava testando a unidade ainda.
Mas quando vamos para testes de integração, ai não tem jeito mesmo, temos que instanciar as implementações (o que ficaria mais simples usando um container de DI), correto?
Abraços!
Oi Marcos,
Sim. Mas repare que, se o exemplo acima fosse feito com TDD, o teste de unidade emergeria naturalmente! Ao fazer TDD, você pensa mais em classes e como elas se relacionam com outras, e não tanto na implementação delas.
E sim, no teste de integração, não tem jeito, você vai passar as implementações concretas de todas as classes. Você pode usar seu framework de injeção de dependência para facilitar nesse trabalho!
Abraços!
Parabens pelo post…
Mas não seria “buscador.pega(String)” ?
Oi Jefferson,
Sim, você está certo! 😉
Abraços,
Mauricio