Saturday, October 18, 2008

Modelando código testável

No mundo do desenvolvimento de software, testes unitários são aceitos por quase 100% dos profissionais, empresas e clientes como uma excelente ferramenta para o incremento da qualidade, manutenibilidade, confiabilidade e mais vários *dade que a literatura tanto gosta de enumerar. Paradoxalmente, quase ninguém aceita arcar com os custos associados à utilização deste tipo de teste quando percebe que eles não são tão triviais como são pintados pela maioria dos TDD Evangelists de plantão.
Em muitos casos, o código de um determinado teste unitário é mais extenso e/ou complexo que o código que está sendo testado. Infelizmente só nos damos conta disso quando começamos a bater cabeça. Como os projetos do mundo real nunca são tão simplórios quanto os exemplos Hello World like dos livros, e os prazos sempre são apertados, a boa intensão de criar aquela suite de testes mágica que vai verificar o código automaticamente de hora em hora, vai por água abaixo.
Outra importante causa de fracasso neste campo é que muita gente não modela o código de forma que o mesmo seja testável. Não é óbvio à primeira vista, mas nem todo código é testável. Usualmente, código bem modelado é testável, mas o desenvolvedor tem que pensar, o tempo todo, que aquela classe tem que ser testada isoladamente, livre de todas as dependências. Assim, o requisito mais importante para utilizar testes unitários como uma ferramenta efetiva, é a modelagem do código de forma que ele seja facilmente testável via frameworks como JUnit ou TestNG. Se o código não atende a este requisito, refatore-o ou esqueça os testes unitários.
Antes de dizer como modelar código testável, prefiro dizer o que torna uma classe difícil de testar:
  • Classes faz-tudo com milhares de linhas contendo métodos faz-tudo com centenas de linhas são muito difíceis de testar. Quando o código a ser testado possui uma grande quantidade de fluxos possíveis, é muito complicado configurar um código de teste que exercite todos estes fluxos. É importante que as classes tenham responsabilidades bem definidas e que elas deleguem outras responsabilidades para outras classes. Isso é modelagem OO tradicional mesmo, nada de novo.

  • Classes que referenciam outras classes (colaborators) diretamente, usando o operador new. É praticamente impossível substituir os colaborators por mocks.

  • Classes que dependem diretamente de recursos externos, tais como: conexões de rede, sistemas de arquivos, datasources, etc são uma pedra no sapato do autor de um teste unitário.

O exemplo a seguir mostra uma classe que depende que um arquivo com um certo nome exista no sistema de arquivos, o que força o pobre desenvolvedor do teste unitário a garantir que o arquivo em questão exista no mesmo diretório, exatamente com o mesmo nome usado pelo código alvo do teste:
public class ImportadorDadosVendas{
  public void importar(){
    File arquivoVendas = new File(obterNomeArquivoVendas());
    //--- Processar arquivo linha a linha, bla, bla
  }
  private String obterNomeArquivoVendas(){
    //
  }  
}
É muito simples refatorar o código acima de modo a torná-lo mais amigável a um teste unitário. Como o sistema de arquivos é um recurso externo, temos que garantir que qualquer acesso a ele seja feito indiretamente, através de uma classe auxiliar. Neste exemplo vou usar o Spring para ilustrar a aplicação da inversão de controle (IoC) neste tipo de situação. O código a seguir ilustra esta refatoração:
/**
 * Interface que define um servico de acesso indireto ao filesysten
 **/
public interface ProvedorArquivos{
  File getReferenciaArquivo(String nomeArquivo);
}

/**
 * Implementação padrão da interface ProvedorArquivos
 **/
public class ProvedorArquivosImpl implements ProvedorArquivos{
  public File getReferenciaArquivo(String nomeArquivo){
    return new File(nomeArquivo);
  }  
}

/**
 * Serviço original refatorado
 **/
public class ImportadorDadosVendas{
  private ProvedorArquivos provedorArquivos;
  public void importar(){
    //--- Referência ao arquivo passa a ser fornecida pelo objeto provedorArquivos
    File arquivoVendas = provedorArquivos.getReferenciaArquivo(obterNomeArquivoVendas());

    //--- Processar arquivo linha a linha, bla, bla
  }
  private String obterNomeArquivoVendas(){
    //
  }

  /**
   * IMPORTANTE: Este método é chamado pelo Spring, que injeta o colaborator
   * O teste unitário pode chamar este método para fornecer um mock
   **/
  public setProvedorArquivos(ProvedorArquivos provedorArquivos){
    this.provedorArquivos = provedorArquivos;
  }
}
Para o Spring, temos a seguinte configuração:

  
  
  

  
  
    
    
    
  

A classe ImportadorDadosVendas agora usa uma implementação da interface ProvedorArquivos para recuperar uma referência ao arquivo que será importado. Note também que a instância do provedor de arquivos não é criada diretamente a partir do código da classe ImportadorDadosVendas, mas é fornecida externamente pelo Spring via um setter. Esta é uma mudança crucial: as dependências não são criadas localmente, mas fornecidas externamente.
Quando a classe ImportadorDadosVendas for executada normalmente, será fornecida uma instância de ProvedorArquivosImpl via setter. Porém, quando for executada por um teste unitário, será fornecida uma implementação alternativa da interface ProvedorArquivos, com comportamento previsível dentro do contexto do teste:
public class ImportadorDadosVendas extends Testcase{
  public void testImportar(){
    ImportadorDadosVendas importador = new ImportadorDadosVenda();
    ProvedorArquivos provedorArquivos = new ProvedorArquivosMock();
    importador.setProvedorArquivos(provedorArquivos);
    importador.importar();
    //--- Asserts a partir daqui
  }

  public static class ProvedorArquivosMock implements ProvedorArquivos{
    public File getReferenciaArquivo(String novoArquivo){
      File arquivoParaTeste = File.createTempFile("testeImportacao.dat");
      //--- Popular arquivo temporario com dados para o teste
    }
  }
}
Esta abordagem pode ser aplicada a qualquer dependência da classe a ser testada. O uso de objetos “falsos”, conhecidos como mocks e stubs é uma das técnicas mais importantes para a implementação de testes unitários. Note que a referência à instância do provedor de arquivos é feita através de uma interface. Isso esconde a verdadeira implementação e facilita a substituição da classe real por um mock. Existem diversas libs de suporte à criação de mocks e stubs. Entre elas, as mais usadas são EasyMock e JMock.
O uso de frameworks de inversão de controle (AKA IoC) como Spring ou Google Guice traz uma grande contribuição para a testabilidade do código. Estes frameworks funcionam como super factories de objetos, sendo responsáveis tanto pela criação dos objetos, quanto dos relacionamentos entre eles. A chave está justamente no gerenciamento das dependências. Uma vez que as dependências são injetadas de forma transparente, via setters, o código do teste unitário pode substituir estas dependências por mocks usando os mesmos setters que normalmente seriam invocados pelo container de IoC.
Está fora do escopo deste post abordar o funcionamento dos containers IoC, mas quero enfatizar que eles são a principal ferramenta no caminho para a criação de código bem estruturado, pouco acoplado e portanto testável.
Criar suites de testes efetivas não é uma tarefa simples, mas pode ser facilitada:
  • Procure dividir a complexidade em classes menores com responsabilidade bem definida.

  • Sempre acesse colaborators através de interfaces.

  • Use frameworks de IoC como o Spring para tornar o mecanismo de injeção de dependências mais fácil de ser substituído pelo setup do código de teste.

  • Isole o acesso a recursos externos como bancos de dados e sistemas de arquivos. Referências a estes recursos devem ser fornecidas por interfaces injetadas via IoC.