quarta-feira, novembro 19, 2008

Melhorando a qualidade e a testabilidade de um sistema com técnicas para quebra de dependências

Em código de sistemas legados (e mesmo os que não deveriam ser tão legados assim!) encontramos módulos, classes ou métodos muito acoplados entre si e difíceis de serem testados unitariamente. Como os grandes papas do desenvolvimento de software explicam, esse tipo de dificuldade de testar possui forte ligação com um design ou arquitetura ruim.

Portanto: Código com alto nível de testabilidade é código bem projetado e código difícil de testar é mal projetado.

Usando técnicas (como refactoring, Test-Driven Development e Design Emergente) e design patterns apropriados podemos evitar um projeto ruim. Mas como melhorar a testabilidade de classes orientadas a objeto que não foram implementadas dessa forma? Para isso podemos usar algumas técnicas e refatorações (refactorings) apropriados. Isso facilitará os testes unitários de classes críticas de seu sistema e ainda, por tabela, melhorará o design de parte de seu sistema.

Para ajudar no entendimento das técnicas, vamos começar pelo exemplo simples de código Java abaixo:

public class TaxaDAO {
public double fetchRate(){
double rate = 0.0;
// Código para acessar o banco de dados e selecionar a taxa de uma tabela.
return rate;
}
}

public class BusinessRule {
private double commissionRate;
private TaxaDAO myTaxaDAO;

public BusinessRule(){
myTaxaDAO = new TaxaDAO();
this.commissionRate = 10.0;
}
public float getRate(){
double baseRate = myTaxaDAO.fetchRate();
return baseRate + (baseRate * commissionRate);
}
}

Neste exemplo, a classe BusinessRule possui comportamento testável e é importante testar o método getRate(). Porém, este depende diretamente de TaxaDAO. Podemos dizer que TaxaDAO é um objeto efêmero. Ele é efêmero porque possui um estado imprevisível. O valor de retorno do método fetchRate() irá variar de acordo com o conteúdo que está no banco de dados naquele momento específico. E essa imprevisibilidade afeta a testabilidade de qualquer objeto que use TaxaDAO. Além disso, não é boa prática criar testes unitários que dependam de operações de custo alto e tempo longo (conectar e acessar um banco de dados é uma delas).

Uma solução para esse problema seria criar uma versão de TaxaDAO que não entra em contado com a base de dados, retornando um valor conhecido sempre. De acordo com Koskela, dependendo do grau de complexidade este objeto alternativo pode ser conhecido como um stub, um fake ou um mock.

Mas ainda temos um problema. Como podemos fazer o objeto BusinessRule usar a versão substituta de TaxaDAO? Da forma como o design do código de produção está, isso é inviável. Aí podemos usar a primeira técnica de refactoring que nos ajudará nessa tarefa gloriosa: a refatoração Extract Interface. No clássico livro original de MartinFowler, a motivação para o uso de Extract Interface é quando várias classes possuem responsabilidades similares. Mas Michael Feathers nos explica que essa refatoração também é uma excelente técnica para quebrar as dependências entre módulos.

Vamos então para esta que é uma refatoração simples e que aplicaremos na classe TaxaDAO (você poderia fazê-la manualmente ou então usar o recurso de uma IDE que suporte refatorações. A última opção é melhor porque economiza seu tempo e seu cérebro... he, he).

public interface ITaxaDAO {
double fetchRate();
}

public class TaxaDAO implements ITaxaDAO {
public double fetchRate(){
double rate = 0.0;
// Código para acessar o banco de dados e selecionar a taxa de uma tabela.
return rate;
}
}

Extremamente simples, não é? Pois é, muitos refactorings são simples de fazer, outros nem tanto. O importante é que precisamos conhecê-los a fundo para sermos bom desenvolvedores de software! Mas voltando ao nosso exemplo depois da divagação...

Ainda não resolvemos o problema da dependência, pois precisamos modificar a classe BusinessRule. Temos duas possibilidades para resolver a quebra de dependência. Vamos mostrar primeiro a técnica de endo-testing.

Para fazer o endo-testing funcionar devemos primeiro aplicar outro refactoring na classe BusinessRule. É a refatoração Extract and override factory method. TEmos que também modificar a classe para utilizar a interface ITaxaDAO ao invés do objeto concreto TaxaDAO.

public class BusinessRule {
private double commissionRate;
private ITaxaDAO myTaxaDAO;

public BusinessRule(){
myTaxaDAO = makeTaxaDAO();
this.commissionRate = 10.0;
}

protected ITaxaDAO makeTaxaDAO(){
return new TaxaDAO();
}

public float getRate(){
double baseRate = myTaxaDAO.fetchRate();
return baseRate + (baseRate * commissionRate);
}
}

E voilá! Criamos um método chamado makeTaxaDAO() que cria o objeto concreto. O construtor agora faz referência apenas à interface ITaxaDAO. De acordo com Feathers, criação "hard-coded" de objetos diretamente no construtor é ruim quando queremos colocar uma classe sob teste. A refatoração Extract and override factory metgod é extremamente poderosa e simples para nos ajudar.

Mas ainda não terminamos. Como realizar essas refatorações nos ajuda a testar o código? Vamos mostrar agora como seria o código do teste unitário da classe BusinessRule, utilizando a técnica de mocks.

public class BusinessRuleTest extends TestCase {
private BusinessRule testBR;
private MockControl control;
private ITaxaDAO mockDAO;

public void setUp(){
control = EasyMock.controlFor(ITaxaDAO.class);
mockDAO = (ITaxaDAO)control.getMock();
testBR = new BusinessRule(){
protected ITaxaDAO makeTaxaDao(){
return mockDAO();
};
}
public void testGetRate(){
mockDAO.fetchRate();
control.setReturnValue(50.00);
control.activate();

assertEquals("Testando getRate()", 50 * .10,
testBR.getRate())
}
}

Que maluquices fizemos no código do teste unitário? Bom, não vou entrar em detalhes sobre o mock (espere os próximos capítulos!). Mas precisamos explicar o que acontece na criação de um novo objeto BusinessRule, para entender a sacada do endo-testing. O que fizemos na criação de BusinessRule foi criar uma inner class anônima que extende BusinessRule, mas que sobrescreve (overriding) o método makeTaxaDAO() para que ele retorne o mock do DAO ao invés do TaxaDAO concreto! Dessa forma, conseguimos resolver o problema da efemeralidade do objeto que acessa o banco de dados e ainda deixamos o design mais testável e orientado a serviços.

Mas esperem! Ainda não terminou :-). Eu comentei que há duas possibilidades para deixar a classe BusinessRule mais testável. Vamos mostrar a segunda técnica agora, com o uso de Dependency Injection. Algumas pessoas não gostam muito de fazer isso. Uma delas é o Scott Bain, autor do grande livro Emergent Design. Ele é fã do endo-testing. Mas, para quem vai usar um container de Inversão de Controle, nada melhor que usar essa segunda técnica. A injeção de dependências nada mais é que uma forma de definir uma classe de modo que ela receba uma dependência externa e não a acople diretamente no código. Vamos a um exemplo disso antes de voltar ao nosso código.

public interface IFoo
{
void bar();
void baz();
}

public class DatabaseFoo implements IFoo
{
void bar()
{
Database.selectBar().execute();
}

void baz()
{
Database.selectBaz().run();
}
}

A classe DatabaseFoo implementa a interface IFoo no código acima. Agora como uma classe poderia usar o objeto DatabaseFoo? O modo típico seria assim:

public class ImportantClass {
IFoo foo;

public ImportantClass()
{
this.foo = new DatabaseFoo();
}

void doReallyImportantStuff()
{
this.foo.bar();
}
}

Fazendo desta forma acima na classe ImportantClass, nós simplesmente detonamos a idéia de interfaces, já que estamos acoplando nosso código diretamente a DatabaseFoo. Como podemos resolver o problema? Injetando a dependência! Abaixo temos o construtor modificado para receber a interface IFoo como parâmetro.

public ImportantClass(IFoo foo)
{
this.foo = foo;
}

Essa é a famosa constructor-based injection. Poderia ser ainda field-based ou setter-based. Mas para o nosso exemplo a injeção via construtor já basta. Vamos retornar então ao nosso código e refatorá-lo para aceitar a injeção de dependência.

public class BusinessRule {
private double commissionRate;
private ITaxaDAO myTaxaDAO;

public BusinessRule(ITaxaDAO umTaxaDAO){
myTaxaDAO = umTaxaDAO();
this.commissionRate = 10.0;
}

public float getRate(){
double baseRate = myTaxaDAO.fetchRate();
return baseRate + (baseRate * commissionRate);
}
}

Perceba que usando essa técnica não precisamos de um factory method. O que fazemos é injetar um objeto que implemente ITaxaDAO diretamente via construtor. Vamos ver então como ficaria o teste de BusinessRule que utiliza injeção de dependência.

public class BusinessRuleTest extends TestCase {
private BusinessRule testBR;
private MockControl control;
private ITaxaDAO mockDAO;

public void setUp(){
control = EasyMock.controlFor(ITaxaDAO.class);
mockDAO = (ITaxaDAO)control.getMock();
testBR = new BusinessRule(mockDAO);
}
public void testGetRate(){
mockDAO.fetchRate();
control.setReturnValue(50.00);
control.activate();

assertEquals("Testando getRate()", 50 * .10,
testBR.getRate())
}
}

Note que agora nosso teste unitário não precisa mais de uma inner class anônima. O que fazemos é injetar diretamente o nosso Mock (lembre que ele implementa a mesma interface ITaxaDAO) através do construtor do objeto BusinessRule!

Alguém pode perguntar então: Legal, no teste unitário é perfeito, mas... e no código de produção? Como faremos a ligação, o acoplamento entre BusinessRule e TaxaDAO? É aí que entram os famosos frameworks de Inversão de Controle (IoC - Inversion of Control) como Spring, Google Guice, NInject, Unity e etc. Eles é que fazem a amarração e injetam as dependências necessárias. Mas esse artigo está muito longo, e não vou entrar em detalhes sobre esse assunto :-) .

Vamos concluir então: um sistema com boa testabilidade costuma ser um sistema mais bem projetado e arquitetado. Conforme detalhado por Meszaros, quando você utiliza técnicas como o Test-Driven Development você naturalmente acaba projetando para testabilidade (design for testability). Mesmo em sistemas legados, com algumas refatorações, podemos melhorar enormemente a testabilidade do sistema e, consequentemente, aumentar sua qualidade interna e externa.

Referências:

Michael Feathers - Working Effectively with Legacy Code
Gerard Meszaros - xUnit Test Patterns
Martin Fowler - Refactoring: Improving the Design of Existing Code
Scott Bain - Emergent Design
Lasse Koskela - Test-Driven: Practical TDD an Acceptance TDD for Java Developers
Artigo na IBM DeveloperWorks - Unit Testing with Mock Objects
Artigo na Wikipedia - Dependency Injection

5 Comentários:

At 10:49 AM, OpenID André Faria disse...

Muito bom Papo!
Um outro livro que eu recomendaria é o "xUnit Patters".

Abraço,
André Faria

 
At 7:28 PM, Blogger Raphael de Almeida disse...

Muito bom, esclareceu minhas dúvidas sobre testes envolvendo banco de dados.

 
At 10:38 PM, Blogger INFORM4TICA disse...

Gosto muito dos artigos de ótima qualidade do seu Blog. Quando for possível dá uma passadinha para ver meu Curso de Informática à Distância. Antonio B Duarte Jr.

 
At 11:05 PM, Blogger Andre disse...

Legal, estamos utilizando técnicas bem similares no trabalho. Parabéns

 
At 2:18 PM, Blogger Shigueru disse...

Parabéns pelo artigo! Muito útil.

Uma outra técnica para viabilizar o teste seria refatorar o trecho

double baseRate = myTaxaDAO.fetchRate();

criando uma função específica para isso, por exemplo, getBaseRate(). Daí no teste unitário utilizar a mesma abordagem da classe anônima, sobrescrevendo somente getBaseRate().

É mais simples pois podemos abrir mão do mock.

As técnicas aqui expostas também podem ser estudadas na tese de mestrado do Eduardo Guerra (http://agilcoop.incubadora.fapesp.br/portal/Artigos/MestradoEduardoGuerra.pdf), vale a pena conferir.

[]s

 

Postar um comentário

Links para este artigo:

Criar um link

<< Home


Veja as Estatísticas