É impressionante como o desenvolvimento orientado a testes transforma o jeito como se escreve código. Também é impressionante como o TDD muda a concepção das coisas. Invertemos nossa forma de trabalhar - agora escrevemos testes antes do código. Nossos compiladores se transformaram de meros "Type checkers" para grandes parceiros nos frequentes processos de red-green-refactor que já estamos tão acostumados a executar. "Faça isso", "faça aquilo"... "crie esta classe", "codifique este método", "implemente esta interface"... Outro dia, ouvindo um podcast do Kent Beck, descobri que o fato do TDD promover a qualidade do código já não é tão importante. Qualidade é um atributo momentâneo. Sobe e desce a cada release liberada. Veja que não estou dizendo que qualidade não é importante. Muito pelo contrário, qualidade é essencial e é fato fundamental para a sobrevivência de qualquer produto. Mas qualidade não deve ser tratada como uma meta a ser alcançada a qualquer custo. Ela é uma consequência natural de um outro atributo ainda mais importante: Saúde. Como um organismo vivo, um software é saudável quando ele se defende bem de agressões externas - mudanças de requisito, novas funcionalidades, desafios tecnológicos. Software parado é software morto. Software que evolui com dificuldade é sofware convalescente.
"My goal is not quality software, my goal is healthy software" (K.Beck).
Mais do que promover a qualidade do software, TDD promove saúde. Great! Um ponto a mais para quem o utiliza. Mas não é só isso... como nos comerciais da Polishop, levando o TDD você ainda leva totalmente grátis a necessidade e rever todo o design de sua aplicação para que você possa se beneficiar da técnica de forma plena. Em resumo, TDD promove um melhor design. Faça a seguinte experiência: Selecione aleatoriamente uma classe qualquer do seu sistema. Você consegue testá-la em alguns minutos? Não? Então sua classe pode ter problemas de acoplamento e/ou coesão (leia mais em Michael Feathers).
Nossa conclusão é que um teste escrito dessa forma é muito mais eficiente:
[Test]
public void LiquidarDocumentoComCaixaAbertoTeste()
{
Id idCaixa = new Id(93);
Id idBoleto = new Id(120922);
Id idUsuario = new Id(2);
const double VALOR_BOLETO = 392;
ResultList list = new MemoryResultList();
CaixaVO caixaVO = FactoryVO.GetCaixaVO(idCaixa, idUsuario, TestEnvironment.Hoje(), TestEnvironment.DataInDefinida());
BoletoVO boletoVO = FactoryVO.GetBoletoVO(idBoleto, TestEnvironment.Hoje(), Boleto.STATUS_ABERTO, VALOR_BOLETO);
Tesouraria operadorTesouraria = new Tesouraria();
operadorTesouraria.LiquidarDocumento(caixaVO, boletoVO, TestEnvironment.Hoje(), VALOR_BOLETO, false, list);
BoletoVO voBoletoVerificacao = (BoletoVO)list.GetObjectFromListById(typeof(BoletoVO), idBoleto);
LiquidacaoDocumentoVO liquidacaoVO = (LiquidacaoDocumentoVO)list.GetObjectFromListByIndex(1);
Assert.AreEqual(Boleto.STATUS_PAGO, voBoletoVerificacao.Status);
Assert.AreEqual(idBoleto, liquidacaoVO.Boleto);
}
Este teste é simples, mas ilustra bem como o design da aplicação pode fazer a diferença. A primeira mudança de paradigma é o uso do Dependency Injection. Push, dont Pull! O objeto que está sendo testado recebe todos os outros objetos que colaboram com ele. Além de permitir a testabilidade da classe, essa técnica ajuda a manter o nível de acoplamento sob controle. Quanto mais objetos uma classe precisar receber, maior será a desconfiança de que algo está errado e de que as responsabilidades precisam ser redistribuídas. O objeto Tesouraria representa uma metáfora de design e sua função é selecionar e orquestrar objetos para a execução de operações co-relacionadas. Como seu estereótipo é de domínio, sua execução não depende de bancos de dados, bibliotecas de terceiros, etc. Ou seja, ela, como todas as outras desse estereótipo, é auto-testável.
E quanto ao acesso ao banco? É difícil, hoje em dia, imaginar uma aplicação que não dependa de um banco de dados ou de um meio de armazenamento qualquer. Fazer acesso a banco em objetos de domínio não é uma boa opção. Em nossa arquitetura, o acesso a banco é feito apenas para alimentar a camada de domínio, mas não faz parte dela. Uma camada de Serviços recupera informações persistidas de uma camada DAO. Para testar a camada de serviços podemos usar Mock Objects ou utilizar um framework de cenários que criamos para gerar um cenário no banco de dados compatível com o teste que se deseja realizar. Veja o código que testa uma classe da camada de serviço com o apoio de um framework de cenários:
[TestFixtureSetUp]
public void CriarCenario()
{
ScenarioManager.RunScenario("001");
}
[Test]
public void ObterBoletoTeste()
{
string sqlPrimeiroBoletoAberto = "SELECT TOP 1 * FROM " + BoletoVO.TABLE_NAME;
sqlPrimeiroBoletoAberto += " WHERE " + BoletoVO.COL_STATUS + " = 'A'";
DataRow dadosBoleto = ScenarioManager.GetInfoInTheFirstRow(sqlPrimeiroBoletoAberto);
Assert.AreEqual(false, dadosBoleto == null, "Deveria existir um boleto no Banco de Dados neste ponto.");
int idBoleto = Convert.ToInt32(dadosBoleto[BoletoVO.PK_NAME]);
ServicosBoleto service = new ServicosBoleto();
BoletoVO boletoVO = service.ObterBoleto(idBoleto);
Assert.AreEqual(boletoVO.Id, new Id(idBoleto));
Assert.AreEqual(boletoVO.DataVencimento, Convert.ToDateTime(dadosBoleto[BoletoVO.COL_DATA_VENCIMENTO]));
Assert.AreEqual(boletoVO.Status, Convert.ToString(dadosBoleto[BoletoVO.COL_STATUS]));
Assert.AreEqual(boletoVO.ValorDocumento, Convert.ToDouble(dadosBoleto[BoletoVO.COL_VALOR_DOCUMENTO]));
}
[TestFixtureTearDown]
public void TearDown()
{
ScenarioManager.CleanUp();
}
O Framework de cenários é configurado em um arquivo Xml, algo como o mostrado a seguir:
<?xml version="1.0" encoding="utf-8" ?>
<Cenarios RootScriptFolder=" \Phidelis.Src\Kernel\Financeiro\ TestesFuncionais\Cenarios\Scripts">
<Script Id="CleanUp_01" Type="CleanUp" Description="Exclusão de todos os registros" File="CleanUp.sql" />
<Script Id="001" Type="Cenario" Description="Ficha Financeira com um boleto de rematrícula aberto" />
</Cenarios>
Este tipo de teste é categorizado como um Teste Funcional e ainda é um teste de desenvolvedor e não um teste de Quality Assurance. No entanto, só é executado sobre a camada de serviços, que é a camada consumida pela Interface de Usuário.
Depois de 2 anos trabalhando com TDD e mais de 1000 testes sendo executados a cada build, chegamos a importantes conclusões com relação ao nosso processo de escrever testes. Apenas com uma mudança arquitetural, poderíamos ter um aumento fenomenal na quantidade e na qualidade dos testes unitários, além de podermos executá-los de forma mais rápida e com mais frequência. As técnicas de Design realmente fazem a diferença, e são os testes unitários que nos têm levado a descobri-las e nos motivado a incorporá-las em nossos sistemas.