TDD e sua influência no acoplamento e coesão

TDD e sua influência no acoplamento e coesão
gas
gas

Compartilhe

Escrever testes de unidade é uma prática cada vez mais adotada. Ela ajuda a verificar se tudo funciona como o esperado mesmo após mudanças, trazendo mais segurança para a equipe ao alterar o código. Mas os testes de unidade vão além, possibilitando a validação de um design.

Um código fácil de testar tende a apresentar um bom design. _Existe uma grande sinergia entre testabilidade e um bom design_. Isso acontece pois, para que o programador consiga testar uma classe de maneira isolada e facilmente, essa classe deve lidar muito bem com suas dependências (buscando sempre um baixo acoplamento) e possuir pouca responsabilidade (ou seja, ser altamente coesa). Caso contrário, o programador gastará muito tempo tentando testá-la, um possível indicador de que a classe apresenta um design pobre.

Banner promocional da Alura, com chamada para um evento ao vivo no dia 12 de fevereiro às 18h30, com os dizeres

Um design que apresenta classes com alto acoplamento são difíceis de testar, surgindo a necessidade de passar as dependências para nossos objetos e só então testá-los.

O primeiro resultado ao adicionar testes de unidade a um sistema é a mudança de padrões imperativos de execução onde instanciamos objetos e invocamos métodos para a injeção de dependências.

O exemplo a seguir mostra um processador sendo instanciado, o que torna difícil verificar se o comportamento do ExecutorDeBatch foi correto sem executar o código da classe Processador:

 public class ExecutorDeBatch { public void le(ListaDeItens itens) { Processador processador = new Processador(); while(itens.temProximo()) { String item = sc.proximo(); processador.interpreta(item); } } } 

O teste acima verifica o comportamento do ExecutorDeBatch junto com o Processador. Esse tipo de teste é considerado de integração, não passando muita informação sobre o acoplamento entre as classes, mas somente garantindo o comportamento em conjunto.

Para isolar a classe acima seria necessário a utilização de um mock no lugar do Processador. Da maneira que o código está, seria bastante trabalhoso. No Java exigiria até manipulação de bytecode durante o classload para que fosse possível alterar a classe instanciada no momento do new Processador() por exemplo. Para isso, o programador deve deixar essas dependências bem explícitas, recebendo-as através do construtor, por exemplo. Teríamos então o seguinte código:

 public class ExecutorDeBatch { private Processador processador; public ExecutorDeBatch(Processador processador) { this.processador = processador; }

public void le(ListaDeItens itens) { while(itens.temProximo()) { String item = sc.proximo(); processador.interpreta(item); } } } 

Passar um mock para a classe acima e consequentemente testá-la de maneira isolada ficou é fácil. A adoção dos testes de unidade deixa explícito o excesso de dependências (ou dependências implícitas escondidas).

Por fim, os testes precisam ser simples e curtos, limitando seu escopo; um teste muito grande ou complicado pode indicar que o método em questão possui muita responsabilidade.

Considere uma classe que é responsável por gerar uma nota fiscal a partir de uma fatura e disparar a mesma para o e-mail do cliente, assim como para um sistema SAP qualquer. Essa classe já possui as dependências bem explícitas:

 public class GeradorDeNotaFiscal { private final Sap sap; private final EnviadorDeEmails enviador;

public GeradorDeNotaFiscal(Sap sap, EnviadorDeEmails enviador) { this.sap = sap; this.enviador = enviador; }

private NotaFiscal geraNotaFiscalAPartirDa(Fatura fatura) { // codigo de geracao da nota fiscal }

public void gera(Fatura fatura) { NotaFiscal nf = geraNotaFiscalAPartirDa(fatura);

sap.armazena(nf); enviador.envia(nf); } } 

Passar mocks para essa classe é fácil, já que é possível injetar todas as dependências nela. O problema é que, para criar testes para ela, o programador se preocuparia em testar se a nota fiscal foi gerada, se ela é enviada ao SAP e se ela é enviada para o cliente:

 @Test public void deveGerarANf() { ... } @Test public void deveArmazenarNfNoSap() { ... } @Test public void deveEnviarNfParaOEmailDoCliente() { ... } 

Perceba que essa classe tem responsabilidades demais: ela gera NF, envia para o SAP, envia para o cliente. Ao escrever esses testes, o programador perceberia que, em cada teste, ele estaria preocupado em testar uma responsabilidade da classe, sem se importar com a outra. No teste deveArmazenarNfNoSap, por exemplo, o programador seria obrigado a passar um stub da classe Email, mesmo não sendo o foco daquele teste. Isso torna a escrita do teste mais massante é um sinal de que a classe tem baixa coesão.

Portanto, se o seu design possue um alto acoplamento ou uma baixa coesão, será praticamente impossível escrever testes de unidade. Por esse mesmo motivo métodos estáticos, singletons, factories e estado global dificultam os testes de unidade.

Pensar e criar os testes antes do código também auxiliam no design. Usar Test-Driven Development força que a concentração do pensamento lógico foque nas responsabilidades de cada método, ao invés da exata implementação do mesmo.

Esse impacto no design é tão grande que muitos vão utilizar a sigla TDD para Test-Driven Design. Com TDD o programador é forçado a pensar em decisões de design antes do código de implementação, criando um maior desacoplamento, deixando mais claro suas necessidades.

O TDD, através dos testes de unidade e refatoração, possibilitam que o design apareça de uma maneira mais evolutiva, mas sem mágicas, sempre dependendo de conhecimento do desenvolvedor em relação a design e orientação a objetos.

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