segunda-feira, 1 de agosto de 2011

Usando DUnit para implementar testes unitários

Esse é meu primeiro post, e vou começar abordando um assunto que ao meu ver é pouco difundido na comunidade Delphi, Testes Unitários, e para isso vou demonstrar como utilizar a ferramenta DUnit para construção dos testes.

Cada vez mais é primordial que o desenvolvedor invista parte de seu tempo na construção de testes, se for utilizando TDD, melhor ainda. Uma boa suíte de teste nos dá segurança e confiança no momento de fazer um refactoring ou a correção de um bug. Também melhora significativamente o design do nosso código, pois, para pode testar de forma realmente unitária, nossas classes precisam ter baixo acoplamento, boa coesão, serem realmente testáveis.



Vou criar um exemplo de calculadora inspirado no padrão Builder seguindo os passos do TDD - Red, Green, Refactor. Primeiro vamos escrever a classe de teste, e está deverá falhar antes da implementação da calculadora em si.
type
 TTestCalculadora = class(TTestCase)¹
 private
 published²
   procedure SomaDe2Mais3DeveSer5;  
 end;

implementation

{ TTestCalculadora }

procedure TTestCalculadora.SomaDe2Mais3DeveSer5;
var
 vCalculadora: TCalculadora;
begin
 vCalculadora := TCalculadora.Create;
 try
   CheckEquals(5, FCalculadora.Soma(2).Soma(3).GetTotal);
 finally
   vCalculadora.Free;
 end;
end;

initialization
  RegisterTest('Calculadora', TTestCalculadora.Suite);³
¹A classe deve herdar de TTestCase, que faz parte do framework DUnit. ²Os métodos que implementam os testes devem ser declarados na seção published, para que possam ser lidos utilizando RTTI. ³A classe deve ser registrada.

Vamos ver se o teste falha. Para os mais puristas, o código não compilar já é o suficiente para a fase Red, eu particularmente, gosto de fazer a implementação suficiente para compilar o código, de qualquer modo, é importante garantir que o teste falhe. Então vou criar a classe calculadora mais não vou implementar o método GetTotal e no método Soma apenas vou retornar a Calculadora para não obter um Access Violation.
type
  TCalculadora = class
  private
  public
    function Soma(const AValor: Double): TCalculadora;
    function GetTotal: Double;
  end;

implementation

{ TCalculadora }

function TCalculadora.GetTotal: Double;
begin
end;

function TCalculadora.Soma(const AValor: Double): TCalculadora;
begin
  Result := Self;
end;
Rodando o teste, o mesmo vai falhar, pois o método retorna um valor qualquer atribuído em tempo de execução. Veja na imagem abaixo.


Agora vamos implementar o método Soma e GetTotal e rodar o teste novamente.
type
  TCalculadora = class
  private
    FValor: Double;
  public
    function Soma(const AValor: Double): TCalculadora;
    function GetTotal: Double;
  end;
  
function TCalculadora.GetTotal: Double;
begin
  Result := FValor;
end;

function TCalculadora.Soma(const AValor: Double): TCalculadora;
begin
  FValor := FValor + AValor;
  Result := Self;
end;
O mesmo deve passar, como na imagem a seguir.


Estamos indo bem, vamos escrever mais um teste, vamos criar um método para resetar o valor. Aqui está o nosso teste.

procedure TTestCalculadora.DeveResetarOValor;
var
 vCalculadora: TCalculadora;
begin
 vCalculadora := TCalculadora.Create;
 try
  vCalculadora.Reset;
  CheckEquals(0, vCalculadora.GetTotal, 'Não resetou o valor');
 finally
   vCalculadora.Free;
 end;
end;

Crie o método Reset na classe calculadora, mais não implemente.

function TCalculadora.Reset: TCalculadora;
begin
end;

Agora rode o teste e você pode ver que... o teste passa. Mesmo sem implementação, ele passa. Por isso é tão importante garantir que o teste falhe antes de implementar, só assim ele tem valor. Um teste que passa antes mesmo de implementar não testa nada, não agrega valor algum. Vamos corrigir isso de modo que o teste falhe.
procedure TTestCalculadora.DeveResetarOValor;
var
 vCalculadora: TCalculadora;
begin
 vCalculadora := TCalculadora.Create;
 try
  vCalculadora.Soma(1).Reset;
  CheckEquals(0, vCalculadora.GetTotal, 'Não resetou o valor');
 finally
   vCalculadora.Free;
 end;
end;

Rode o teste e veja que o mesmo vai falhar. Feito isso vamos implementar o método Reset adequadamente para que o teste passe.
function TCalculadora.Reset: TCalculadora;
begin
  //Evitando número mágicos
  FValor := ZeroValue;
  Result := Self;
end;

Ainda há vários recursos do DUnit a serem explorados mais vou deixar isso para um próximo post, onde mostrarei também como testar exceções conhecidas.

Não deixem de postar sobre dúvidas ou sugestões e escrevam testes. ;D

Abraços.

2 comentários:

  1. E ai Fabricio !

    Tudo bom ?

    Que bom que você começou esse blog... Assim posso acompanhar o seu trabalho mesmo não estando mais trabalhando junto...

    Já adicionei o feed no google readers...

    Att,

    Sakamoto

    MyTraceLog - Registro de um DBA
    http://mytracelog.blogspot.com

    ResponderExcluir
  2. Obrigado por acompanhar.

    Vou me esforçar para manter uma frequência legal de posts e variar um pouco os temas.

    Abraços

    ResponderExcluir