Testando automatizada com DUnit

Errar é humano – Teste automatizado de Delphi com código DUnit. Por Kris Golko

Introduzir erros é humano, o problema não é, normalmente, como corrigi-los, mas encontrá-los antes dos seus clientes. Ferramentas de teste automatizadas vieram para o resgate, com a sua capacidade para repetir o processo de teste continuamente, assim desenvolvedores podem trabalhar com muito mais confiança. Por que escolher DUnit? Porque é sobre testes unitários (unitário, no sentido de construir um bloco de código, em vez de uma unidade Object Pascal). Outra vantagem é que testes DUnit são frameworks no âmbito dos quais executam o código, que é mais rápido e mais conveniente do que executando um aplicativo. O que eu mais gosto em DUnit é que eu posso criar os meus cases de teste com o meu instrumento de desenvolvimento favorito. DUnit suporta Delphi 4 a 7, bem como Kylix.

Primeiros passos

DUnit é distribuído como código fonte, que compreende uma série de fontes Delphi de arquivos, exemplos e documentação. Ele pode ser baixado da página do projeto SourceForge sourceforge.net:/projects/DUnit. Para instalar, basta descompactar o arquivo em um diretório de sua escolha, preservando subdiretórios. As unidades usadas para criar e executar testes estão no subdiretório 'src' que deverá ser adicionado ao Library Path em Environment Options | Library ou o caminho de pesquisa do Project| Options no seu projeto teste.

Como escrever casos de teste

Isso é fácil: tudo que você tem a fazer é criar uma classe herdada do TTestCase, TTestCase é declarado na unidade TestFramework. Para adicionar testes a uma classe de caso de teste, basta adicionar um procedimento publicado para cada teste à sua classe derivada. Um objeto de teste runner usa RTTI para detectar que estão disponíveis em uma classe de caso de teste.

Existem dois métodos muito úteis: Setup e TearDown. Execução de cada caso de teste individual dentro de uma classe começa com o Setup, em seguida, vem o procedimento publicado que constitui o teste, que será seguido por TearDown. Setup pode ser usado para instanciar objetos necessários para executar testes e inicializar os seus estados de acordo com as operações do teste. Setup deverá conter também as ações necessárias a serem realizadas antes de cada teste ser executado, como a conexão com um banco de dados. O método TearDown deve ser utilizado para limpar e eliminar os objetos.

Um procedimento publicado normalmente contém várias chamadas ao método Check. O método Check é declarado como segue:

procedure Check(Condition: boolean; msg: string = ''); virtual;


A condição parâmetro geralmente está sob a forma de uma expressão, que avalia para um valor Booleano. Se a expressão que passou no procedimento Check é avaliada como false, então o teste é marcado como falhou e a execução do teste é abortada.

Check(Expected = Actual, 'Actual number different than expected');


Os testes são organizados em grupos, chamados suites. Testes suites podem conter testes múltiplos e outros testes suites, proporcionando, assim, uma forma de construir uma árvore de testes. Centralizado nas operações do DUnit  está o teste registro, o qual mantém todas as suites no aplicativo do teste. Normalmente, o teste Registro é construído enquanto um teste de aplicativo se inicializa. Unidades que declaram os casos de teste, por convenção, têm uma seção de inicialização onde os casos de teste são criados e adicionados ao registro.

Teste suítes podem ser criados por instanciando a classe TTestSuite declarada no TestFramework. A maneira mais conveniente e muitas vezes usada para criar uma teste suíte é a utilização do método Suite da classe TTestCase, que cria um novo TTestSuite contendo apenas o TestCase: note que ele é um método de classe. 

Os processos de RegisterTest e RegisterTests adicionam testes ou testes suites para o registro. O exemplo mais simples é criar um teste suite contendo um único caso de teste e, em seguida, registrá-la como segue:

Framework.RegisterTest(TMyTest.Suite);


Como executar testes

DUnit inclui dois testes de execução padronizados: TGUITestRunner com interface gráfica interativa GUI e TTextTestRunner com interface de linha de comando modo batch. TGUITestRunner é declarado na unidade GUITestRunner junto com o processo autônomo RunRegisteredTests, que executa os testes suites registrado utilizando TGUITestRunner. A versão do TGUITextRunner CLX é declarada na unidade QGUITestRunner.  

TTextTestRunner é declarado a unidade TextTestRunner em conjunto com o procedimento autônomo correspondente RunRegisteredTests. Solicitações para o RunRegisteredTest são geralmente qualificadas com um nome de unidade, uma vez que existem múltiplos processos globais RunRegisteredTests, por exemplo:

GUITestRunner.RunRegisteredTest;


Usuários existentes de DUnit vão notar que uma das mais recentes alterações ao DUnit é a adição dos métodos de classe RunRegisteredTests e a desaprovação do autônomo RunRegisteredTests, uma vez que vários processos globais com o mesmo nome desnecessariamente desorganizam o espaço para nome. A chamada para RunRegisteredTests agora é recomendada para ser qualificado com a classe de nome teste runner, em vez de um nome de unidade:
 

TGUITestRunner.RunRegisteredTest; 


Um exemplo

Vamos colocar tudo em prática. O aplicativo exemplo recolhe classificações na forma como muitos sites da Web fornecem uma forma de "avaliar este site". Mais concretamente, CodeCentral possui um interessante sistema de classificação de onde é possível definir opções de acordo com suas preferências pessoais (o meu favorito é antiga mitologia grega). A base lógica do nosso sistema de avaliação é definida pela interface IRateCollector.

IRatingCollector = interface

  function GetPrompt: string;

  procedure Rate(Rater: string; Choice: integer);

  function GetChoiceCount: integer;

  function GetChoice(Choice: integer): string;

  function GetChoiceRatings(Choice: integer): integer;

  function GetRatingCount: integer;

  function GetRating(Index: integer; var Rater: string): integer;

  function GetRatersRating(Rater: string): integer;

end;


A classe TRateTests nos permite desenvolver e testar a lógica do negócio de forma independente do desenvolvimento da interface do usuário. É realmente vantajoso desenvolver e testar a lógica do negócio, antes de iniciar a interface de usuário. Os casos de teste são concebidos para testar a implementação de qualquer IRatingCollector.

TRateTests = class(TTestCase)

private

  FRateCollector: IRatingCollector;

protected

  procedure Setup; override;

  procedure TearDown; override;

published

  // tests here

end;


Os procedimentos Setup e TearDown são utilizados para criar uma instância e dispor de uma implementação do IRatingCollector. 

const

  SAMPLE_RATE_PROMPT = 'Rate DUnit (Mythological Slavic Feminine)';

  SAMPLE_RATE_CHOICES: array[0..3] of string = ('Lada', 'Jurata', 'Marzanna', 'Baba Jaga');



procedure TRateTests.Setup;

begin

  // modify this line only to test another implementation of IRatingCollector

  FRateCollector := TSimpleRatingCollector.Create(SAMPLE_RATE_PROMPT, SAMPLE_RATE_CHOICES);

end;



procedure TRateTests.TearDown;

begin

  FRateCollector := nil;

end;


Procedimentos declarados na seção publicada são as unidades básicas de teste; escrevê-las requer criatividade e inventividade. No nosso exemplo, TestChoices verifica se a lista de opções está como esperado.

procedure TRateTests.TestChoices;

var

  I: integer;

begin

  Check(FRatingCollector.GetChoiceCount = Length(SAMPLE_RATE_CHOICES),

     Format('There should be exactly %d choices',

        Length(SAMPLE_RATE_CHOICES)]));

  for I := 0 to FRatingCollector.GetChoiceCount - 1 do

    Check(FRatingCollector.GetChoice(I) = SAMPLE_RATE_CHOICES[I],

          'Expected ' + SAMPLE_RATE_CHOICES[I]);

end;


O procedimento TestRate verifica se executar procedimento da avaliação resulta em um aumento do número de avaliações para a escolha avaliada:

Procedure TestRate

...

  FRatingCollector.Rate(NextRater, 0);

  Check(FRatingCollector.GetRatingCount = RatingCount + 1,

      'Expected ' + IntToStr(RatingCount + 1));

...

end;


Os testes devem ser o mais abrangente possível, mas é muito difícil cobrir todos os cenários possíveis. Enquanto bugs estejam sendo relatados, os testes devem ser revistos.

É muito importante que os testes cubram as condições extremas; no nosso exemplo, a escolha ou o avaliador que passou ao procedimento de avaliação pode ser inválido. Os testes verificam se uma exceção é levantada quando se prevê uma exceção. O seguinte código verifica se a exceção EinvalidRater é levantada quando um avaliador tenta avaliar pela segunda vez.

ErrorAsExpected := false;

Rater := NextRater;

try

  FRatingCollector.Rate(Rater, 0);

  FRatingCollector.Rate(Rater, 0);

except

  // exception expected

  on E:EInvalidRater do

    ErrorAsExpected := true;

end;

Check(ErrorAsExpected, 'Exception expected if a rater has already rated');


Finalmente, o registo do teste, na seção de inicialização:

RegisterTest('Basic tests', [TRateTests.Suite]);


O arquivo do projeto mostra a típica maneira de executar testes no modo GUI.

GUITestRunner.runRegisteredTests; 


Para usar o CLX em vez de usar o VCL, substituia GUITestRunner com QGUITestRunner em chamadas qualificadas para runRegisteredTests, bem como na cláusula de usos.

Plataforma cruzada de programação extrema

Quando se faz a portabilidade DUnit para Kylix, existem duas categorias de problemas, a diferença entre as chamadas de sistema Windows e Linux e as diferenças entre VCL e CLX. Surpreendentemente, cobrir as diferenças OS é mais fácil. O primeiro passo consiste em colocar declarações condicionais nas cláusulas de uso, unidades como o Windows ou mensagens não estão, naturalmente, disponíveis nos Kylix; protótipos do sistema básico de chamadas podem ser encontrados na unidade libc e definições básicas tipo nas unidades padrão Qt e Types. Algumas funções de sistema foram substituídos pelos equivalentes no Linux, outras têm de ser implementadas.

Levou uma série de truques para portabilizar para CLX. Componentes visuais VCL e CLX são apenas ligeiramente diferentes, mas, apesar disto, por vezes a portabilidade pode ser bem difícil.

DUnit para Delphi e Kylix compilam a partir da mesma fonte, com exceção do GUITestRunner / QGUITestRunner GUI teste runners, existem, contudo, um lote de declarações condicionais na fonte para permitir o suporte a esta plataforma cruzada.

Este é apenas o começo

Depois que o básico tiver sido dominado, há muitos recursos adicionais dentro do DUnit mais complexos, que permitem testes a serem realizados. Por exemplo, existem classes já prontas, que podem ser usadas para um fim específico, como os testes de uma perda de memória. A unidade TestExtension contém um conjunto útil de classes baseadas no padrão de design Decorator. Uma das mais importantes é a classe TRepeatedTest, que permite que você execute um caso de teste um determinado número de vezes. Neste exemplo, TRepeatedTest é usado para chamar o procedimento de avaliação várias vezes em sucessão.

RegisterTest ( «Repetidas Rate", 
TRepeatedTest.Create (TRateTests.Create ( 'CheckRate'), 5)); 


A classe TGUITestCase, apóia o teste da interface GUI. TGUITestCase é declarada na unidade GUITesting. Veja a unidade RateGUITests para um exemplo de utilização para testar a caixa de diálogo para apresentar a avaliação.

Como fonte para o Delphi DUnit está livremente disponível, um programador Delphi experiente pode facilmente extrender o DUnit, por exemplo através da criação de novas extensões.

Testando como uma arte libertadora

Testes trazem melhores resultados se os testes são baseados em conhecimentos de design do aplicativo. Se diagramas UML foram criados, eles podem ser usados como base para a construção de testes, completando assim o requerimento, análise, implementação, ciclo de testes.


Idealmente, os testes devem ser desenvolvidos ao mesmo tempo em que o desenvolvimento do código do projeto. Tecnicamente, os testes podem ser criados para os pedidos que já estão completos, no entanto, estas aplicações muitas vezes não são adequadas para testes unitários, uma vez que não possuem uma estrutura modular bem desenvolvida. Utilizar testes automatizados com DUnit promove um melhor design do aplicativo, bem como torna mais fácil  código refator, mas acho que isso faz a maior diferença é na fase da manutenção do aplicativo. Mantenedores podem ser atribuídos às unidades (no sentido de módulos) em vez de concluir projetos e eles podem corrigir erros e testar unidades sem construir e testar um aplicativo inteiro. Às vezes, um problema pode ser resolvido no nível da unidade e o envolvimento de um experiente desenvolvedor é necessário, mas no caso de um grande aplicativo amórfico, desenvolvedores experientes têm de ser envolvidos o tempo todo.