sábado, 6 de dezembro de 2008

Funcionamento dos frameworks xUnit - Parte 1/2

Durante a série de artigos sobre TDD, tenho citado o uso de frameworks de testes unitários baseados na JUnit. Os exemplos que serão exibidos durante a série partem do princípio que o leitor conhece o funcionamento de um framework xUnit.
Sendo assim, coloco aqui uma explicação breve sobre a estrutura da maioria destes frameworks, de forma a facilitar o entendimento dos próximos artigos.


Os chamados frameworks xUnit surgiram do framework SUnit, construído para a linguagem Smalltalk, por Kent Beck e Erich Gamma. A partir daí, o framework foi portado para diversas linguagens (é praticamente certa a existência de um framework para a sua linguagem favorita) e sua adoção cresceu exponencialmente, sobretudo por projetos que adotaram TDD (como a maioria dos projetos XP).

Idéia

A idéia do framework é facilitar a criação, agrupamento e execução de testes unitários, permitindo sua automatização e a aplicação de seus conceitos. Como seu uso, podemos executar, de uma só vez, os seguintes testes:
  • Teste unitário: verifica se cada pequena parte do programa funciona corretamente.
  • Teste de integração: verifica se as partes do programa funcionam corretamente quando utilizadas conjuntamente.
  • Teste de regressão: verifica se uma alteração numa parte do programa afeta o funcionamento de outras partes, inclusive as não relacionadas diretamente.
Ao permitir executar estes testes de forma simples e rápida, sua adoção como parte do processo de construção do programa se torna indolor. Por exemplo, a execução dos testes pode ser feita após cada linkagem do programa, de forma automática, bastando configurar a ferramenta que gerencia o processo de compilação e linkagem para executar os testes ao final.

Com isso refatoração de partes do código se torna muito mais segura e quaisquer falhas no código são detectadas imediatamente, dando um feedback rápido para o desenvolvedor.

Funcionamento Básico

Um projeto de testes baseado na xUnit, poderá exibir sua saída em modo gráfico ou em modo texto, dependo de sua implementação padrão. A maioria gera saída em modo texto, mas em quase todos existem implementações de saídas em modo gráfico que podem ser adquiridas gratuitamente pela Internet.

O framework funciona a partir da criação de um grupo de testes e da execução dos testes nele contidos. Dentro deste grupo de testes, é permitido adicionar, além dos testes, outros grupos. Ao disparar o método que executa os testes contidos em um grupo, ele executará seus testes e solicitará aos seus subgrupos que também executem seus próprios testes. Assim, todos os testes da hierarquia serão executados. No ponto de entrada do programa, como uma função main, geralmente haverá um grupo de testes principal que iniciará todo o processo.

Como funciona um teste

Cada teste criado deve fazer uma verificação sobre o estado ou comportamento de um determinado código. Nas xUnits, essa verificação é feita utilizando métodos ou macros do tipo assert, que, em sua maioria, recebem uma variável booleana como parâmetro e lançam uma exceção caso o valor da variável seja falso. A exceção dá informações adicionais, como o número e o conteúdo da linha de código onde estava sendo chamado o assert. Por exemplo:

assert( 1 > 2 );

Como o resultado da condição é false, uma exceção é lançada informando algo como "Exceção lançada na linha 1. Condição não cumprida: 1 > 2".

Cada método deverá verificar uma e só uma funcionalidade, que poderá compreender em um ou mais asserts. Por exemplo:

void CalculaMaiorDeDoisCorretamente()
{
assert( 2 == MaiorDeDois( 1, 2 ) );
assert( 0 == MaiorDeDois( 0, 0 ) );
assert( -1 == MaiorDeDois( -1, -2 ) );
}
O teste verifica se a função MaiorDeDois funciona com esperado. Caso algum assert falhe, é exibida a linha de código com a expressão que falhou, como descrito no exemplo anterior.

Originalmente, em praticamente todas as linguagens, um assert interrompe a execução do programa gerando um erro, que decreve a condição não cumprida. Para que o assert não funcione desta maneira, os frameworks geralmente criam um novo assert, como sendo um método da classe de teste a qual herdamos. Esta lança uma exceção ao invés de interromper a execução do programa.

Onde são colocados os testes

Cada teste deve ser colocado em um grupo de testes, que é chamado formalmente de Suíte de Testes (Test Suit). Geralmente nos frameworks xUnit, existe uma classe chamada TestSuit que será como uma lista de (ponteiros para) métodos de teste que você escreverá.
Você deverá adicionar (o endereço de) cada método de teste que você escrever à um objeto de TestSuit. Posteriormente, ela poderá executar todos os métodos que você adicionou - um após o outro. Havendo algum assert dentro deles que não teve sua condição satisfeita, o programa termina com uma exceção (conforme descrito acima).

Na verdade, os métodos que você criou não poderão ser adicionados diretamente no TestSuite. Ao invés disso, você deve criar um objeto de uma classe chamada TestCaller (a "chamadora do teste") que terá o nome do teste e um ponteiro para (o endereço d)ele. Algo como:

TestCaller< MinhaClasseTeste > *chamadaTeste =
new TestCaller< MinhaClasseTeste >(
"NomeDoSeuMetodoDeTeste",
&NomeDoSeuMetodoDeTeste
);

Para simplificar este código, é preferível declarar um tipo que mapeie o TestCaller ao tipo de nossa classe:

typedef TestCaller< MinhaClasseTeste > Chamada;

Agora, substituindo no código anterior, fica:

Chamada *chamadaTeste = new Chamada(
"NomeDoSeuMetodoDeTeste", &NomeDoSeuMetodoDeTeste
);

Agora, para criarmos nossa Suíte de Testes, faremos:

Test* MinhaClasseTeste::suite()
{
Test *suite = new TestSuite( "MinhaClasseTeste" );

Chamada *chamadaTeste = new Chamada(
"NomeDoSeuMetodoDeTeste", &NomeDoSeuMetodoDeTeste
);
suite->addTest( chamadaTeste );

// Ou adiciona diretamente, como neste outro metodo
suite->addTest( new Chamada(
"MeuOutroMetodoDeTeste", &MeuOutroMetodoDeTeste
) );

return ( suite );
}

Existe uma classe chamada TestCase, que agrupa logicamente testes que verificam o comportamento de uma determinada funcionalidade. Ela possui um construtor que recebe uma string que representa o nome do caso de teste, para facilitar sua identificação, os métodos setUp e tearDown para implementação de fixtures, além de um método estático (para quem não conhece métodos estáticos, pense somente como se fosse um método que você pode acessar sem precisar instanciar um objeto da classe) chamado suite, que retorna um (objeto de) TestSuite - o objeto que conterá os nossos testes.
Quando precisarmos criar nossos testes, podemos criar uma classe filha de TestCase. Ela conterá todo o comportamento necessário para a execução de testes e nossos métodos de teste, com seus asserts.

Exemplo prático

Assim, para criar um novo caso de teste chamado TesteContaBancaria, faremos:
  1. A declaração da classe como filha de TestCase;
  2. A declaração nossos testes, preferencialmente na parte privada da classe, pois os mesmos não serão acessados por outras classes;
  3. A implementação dos testes;
  4. A implementação de um método estático suite que criará um TestSuite e adicionará à ele (o endereço de) todos os nossos testes;
Os métodos setUp e tearDown não precisam ser implementados, a menos que você precise fazer um fixture (que veremos mais tarde).

class TesteContaBancaria : public TestCase
{
public:
TesteContaBancaria(std::string nome);

// Suite de testes
static Test* suite();

private:
// Metodo de teste
void ContaTransfereCorretamente();
};


TesteContaBancaria::TesteContaBancaria(
std::string nome
) : TestCase( nome ) // Chama o construtor de TestCase
{
}


Test* TesteContaBancaria::suite()
{
// Cria a suite de testes
Test *suite = new TestSuite( "TesteContaBancaria" );

typedef TestCaller< TesteContaBancaria > Chamada;

// Adiciona o metodo ContaTransfereCorretamente para
// ser chamado pela suite
suite->Add( new Chamada(
&ContaTransfereCorretamente,
"ContaTransfereCorretamente"
));

return ( suite );
}


void TesteContaBancaria::ContaTransfereCorretamente()
{
ContaBancaria minhaConta, suaConta;

minhaConta.DefinirSaldo( 1000.00 );
suaConta.DefinirSaldo( 0.00 );
minhaConta.Transferir( suaConta, 400.00 );

CHECK_EQUALS( 400.00, suaConta.Saldo() );
CHECK_EQUALS( 600.00, minhaConta.Saldo() );
}


Obs: A macro CHECK_EQUALS aqui foi criada sobre assert, e recebe o valor esperado e o valor atual. Se os valores diferirem, uma exceção será lançada. Existem diversas outras macros disponíveis para auxiliar na verificação de estados, dependendo da linguagem utilizada.

Repare que o método de teste ContaTransfereCorretamente é privado. Isto se deve ao fato que ele nunca será acessado diretamente fora da classe e também não se espera que a classe de teste tenha filhas, que irão chamar o método diretamente. Assim, costuma ser um padrão de TDD colocar os métodos de teste como sendo privados.

Este é o funcionamento básico de um framework xUnit. Na parte 2 irei expor o uso de fixtures e mostrarei outros métodos/macros úteis para as verificações de estado. Até lá.

Nenhum comentário: