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.

16 Comentários

  1. Tiago Peczenyj 01/03/2011 at 16:43 #

    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?

  2. Rafael Ponte 01/03/2011 at 16:57 #

    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.

  3. Guilherme Silveira 01/03/2011 at 17:54 #

    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

  4. Renato 01/03/2011 at 19:59 #

    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?

  5. Mauricio Aniche 01/03/2011 at 20:08 #

    Oi Renato,

    Perfeito, já corrigi no post!

    Abraços,
    Mauricio

  6. Guilherme Garnier 02/03/2011 at 09:24 #

    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.

  7. Marcos Vinícius da Silva 02/03/2011 at 13:30 #

    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

  8. Guilherme Silveira 02/03/2011 at 16:25 #

    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!

  9. Marcos Vinícius da Silva 02/03/2011 at 18:57 #

    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!

  10. Mauricio Aniche 02/03/2011 at 20:00 #

    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!

  11. Jefferson 09/06/2011 at 10:53 #

    Parabens pelo post…

    Mas não seria “buscador.pega(String)” ?

  12. Mauricio Aniche 09/06/2011 at 11:46 #

    Oi Jefferson,

    Sim, você está certo! 😉

    Abraços,
    Mauricio

Deixe uma resposta