sexta-feira, 27 de novembro de 2009

Guicifique seus Testes - Parte 2

Este artigo é a segunda parte da tradução para português do interessante artigo Guicing Up Your Testing, June 21, 2007, por Dick Wall. Trata-se de uma leitura interessante, que junta dois tópicos que estou estudando atualmente: Teste Unitário e Google Guice, além de falar um pouco sobre Injeção de Dependências e os Padrões de Projeto Singleton e Factory.

Índice

Introdução à Injeção de Dependências

A solução mais simples é eliminar a dependência com o singleton. Para testar a classe Pedido, não há necessidade de realizar uma chamada real a um banco de dados, mesmo que o banco de dados seja rápido o suficiente. O que precisamos na classe GerenciadorTaxasJurosBD são dois métodos: atribuiTaxaParaCliente e verificaTaxaParaCliente. Este último usa o ID do cliente e retorna um double. Esta funcionalidade pode ser fornecida facilmente por um HashMap ao invés de ter que consultar o banco de dados, sendo a implementação com HashMap? mais rápida e leve.

A técnica de criar uma implementação de uma classe como esta, que imita o comportamento desejado para a implementação real, é conhecida como Fake Objects. É diferente da técnica Mock Objects, que veremos adiante (a técnica Mock Objects tem mais a ver com prover ao objeto um roteiro de passos que se espera que ele execute, e verificar se o roteiro foi realmente obedecido).

Voltando ao assunto, o que importa aqui é que para testar a classe Pedido e o seu cálculo para taxar uma determinada venda, não importa quem faz o trabalho de verificar qual o valor da taxa aplicável ao Cliente. Assumindo que o único trabalho de um GerenciadorTaxasJuros seja armazenar e recuperar a taxa usando o ID do cliente, podemos trabalhar com a seguinte implementação:



Há algumas coisas para observar aqui. Primeiro, esta implementação é bem simples. Mais importante, ela armazena tudo em memória, nunca chegando nem perto de um banco de dados, então ela tende a ser realmente eficaz para se utilizar em um teste unitário. Todos os dados necessários podem ser configurados no próprio teste, sem necessidade de armazenar nada no banco de dados.

A segunda coisa é que agora a classe agora implementa a interface GerenciadorTaxasJuros. Para a injeção de dependências funcionar, é necessário criar uma interface que a classe Pedido possa usar, ao invés de expor dentro dela uma implementação específica. A interface deve se parecer com o código abaixo:


Agora, na classe Pedido, podemos substituir todas as referências à classe GerenciadorTaxasJurosBD pela interface GerenciadorTaxasJuros. Isto nos traz toda sorte de benefícios além da testabilidade, que irei cobrir em futuros artigos?.

Em poucas palavras, tudo o que fizemos até aqui foi para eliminar uma dependência que o código tinha com uma implementação. Pedido agora não está mais amarrado a uma classe que o obriga a acessar o banco de dados. Ele agora se comunica através de uma interface, não importando para ele qual a implementação real que vai utilizar, desde que a implementação cumpra o contrato que é estabelecido pela interface.

É claro, agora é necessário, alguma forma, prover para o pedido uma implementação de GerenciadorTaxasJuros. Em produção, iremos fornecer para o pedido a implementação GerenciadorTaxasJurosBD, mas em ambiente de teste, podemos oferecer GerenciadorTaxasJurosFake, que é mais eficaz nexte contexto.

A forma mais fácil de fazer isso (sem ainda utilizar uma framework como o Guice), é simplesmente injetar a implementação desejada da interface GerenciadorTaxasJuros através do construtor da classe Pedido:


A partir de agora, todas as vezes que um pedido for criado

  • em ambiente de produção, podemos injetar um objeto GerenciadorTaxasJurosBD
  • em ambiente de testes, podemos injetar um objeto GerenciadorTaxasJurosFake, que é mais eficaz.

Se você conseguiu entender o conceito acima, você agora compreende tudo o que precisa saber sobre Injeção de Dependências. É apenas uma maneira especial de dizer para o pedido "Eu vou dizer onde você vai buscar os dados quando eu te configurar". Se isso é tudo o que tenho que falar sobre injeção de dependências, então onde é que entra o Guice?

Pense no seguinte: se você tiver que usar o construtor em toda parte em sua aplicação, o que isto acarretará ao seu código? Por um lado, você agora terá bastante trabalho adicional, tendo que passar pra lá e pra cá os objetos que deseja injetar através da aplicação. Toda vez que precisar de um Pedido, onde quer que seja na aplicação, terá de injetar manualmente uma implementação da interface GerenciadorTaxasJuros.

Posso até conseguir alguma forma mais prática de fazer isso, mas provavelmente ela provocará o mesmo tipo de inflexibilidade / acoplamento que existia quando usávamos meramente um Singleton. Logo logo o código inteiro vai estar infestado de parâmetros sendo passados via construtores ou algum outro bacalhau que vai deixar o código cada vez mais sujo.

Além disso, apesar de que um Singleton que tantas pessoas usam em aplicações Java seja um estorvo, o fato de se reutilizar um único objeto GerenciadorTaxasJuros ao invés de ter que criar um novo cada vez que precisar passá-lo ao construtor da classe Pedido, este conceito ainda é bom e desejado.

Sem falar nos problemas relacionados a Thread Safety? ou gargalos de desempenho, instanciar mais de um objeto GerenciadorTaxasJuros - um para cada Pedido - em uma única JVM seria um desperdício de memória, uma vez que todos eles realizam exatamente o mesmo trabalho (e nem mencionamos o quão caro é construir novas conexões no caso da implementação que acessa o banco de dados).

Portanto, enquanto a Injeção de Dependências resolveu o problema de alto Acomplamento, ela introduziu muito mais problemas, e não é nada prática em aplicações de grande porte. É por este motivo que outros Padrões de Projeto?, como Fábricas? e Dicionários?, entre outros, se tornaram tão populares. Eles fornecem uma forma mais organizada de localizar utilizar objetos através da aplicação.

O problema com os dicionários e fábricas é que eles são pesados e normalmente são difíceis de configurar. Eles desempenham um importante papel em grandes sistemas coporativos, mas podem ser inviáveis de se utilizar em pequenos projetos. A maioria utiliza busca de objetos por nome através de Strings, o que carrega seus próprios riscos - Strings incorretas não podem ser capturadas pelo compilador e só vão manifestar problemas em tempo de execução.

O que precisamos, portanto, é de alguma forma de "conectar" (wiring) todas essas dependências entre objetos, e esta conexão deve ser feita em tempo de execução, para evitar alto acoplamento no código. Devemos ser capazes de configurar em algum lugar qual implementação precisamos utilizar em cada circunstância, e deixar que alguma biblioteca faça o trabalho de conectar tudo, seja para testes ou para produção.

Nenhum comentário:

Postar um comentário