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 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!

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