Fenner
Compilador

Testes com [Test] e Fenner.Testing

Atributos [Test], [Setup], [TestCase] e [Ignore], o Assert, o relatório e o código de saída; como o compilador descobre os testes.

O que é e por que existe#

Um teste unitário é uma rotina que chama o código e confere o resultado. O Fenner usa atributos para marcá-las e um framework pequeno, o Fenner.Testing, para rodá-las. A descoberta dos testes acontece na compilação: um meta module (Fenner.TestingMeta) varre as rotinas marcadas e gera o TMain.RunAll. Não há reflexão em tempo de execução nem lista de testes para manter.

Fenner
uses System.Remainder, Fenner.Testing, Fenner.TestingMeta;

type
  TMain = class(TTestSuite) end class;           // o meta module gera TMain.RunAll

[Setup] procedure Prepare;                  // roda antes de CADA teste
begin
  GBase := 10;
end procedure;

[Test] procedure DivideWorks;
begin
  Assert.AreEqual(5, Divide(GBase, 2));
end procedure;

begin
  Halt(TMain.Create.RunAll);                // 0 se tudo passou, 1 se algum falhou
end.

Os módulos Fenner.Testing e Fenner.TestingMeta moram em Compiler\testing, fora da RTL: o .config precisa pôr essa pasta no SourcePath.

Atributos#

AtributoEfeito
[Test]marca um procedimento sem parâmetros (do módulo, ou método de classe: o fixture é criado com Create sem argumentos, um novo por teste)
[Setup] / [Teardown]rodam antes / depois de cada [Test] do mesmo módulo e da mesma classe (o Teardown em finally)
[OneTimeSetup] / [OneTimeTeardown]uma vez por suíte
[TestCase(2, 3, 5)]repete o teste com cada linha de dados; os valores viram os parâmetros
[Ignore] / [Ignore('motivo')]pula o teste e mostra [SKIP]

Assert#

Fail(msg), IsTrue/IsFalse(cond[, msg]), AreEqual (inteiros, cadeias, ou reais com tolerância: AreEqual(0.3, 0.1 + 0.2, 0.0001)), AreNotEqual, IsNil/NotNil, e Throws(TipoDaExceção, procedure; begin … end) para exigir que uma exceção seja levantada. Falha de asserção é ETestFailure; qualquer outra exceção vira [ERRO].

Tests.fxp
program Tests;
// Demo: testes unitários com atributos ([Test]) e o framework Fenner.Testing (Compiler\testing).
// O compilador varre as rotinas marcadas e gera TMain.RunAll; o relatório sai na saída padrão.
// O Tests.config coloca Compiler\testing no SourcePath.

uses System.Remainder, Fenner.Testing, Fenner.TestingMeta;

// ---- código sob teste ----

function Divide(A, B: Int32): Int32;
begin
  if B = 0 then
    raise Exception.Create('divisão por zero');
  Exit(A div B);
end function;

function IsPalindrome(const AText: UString): Bool8;
var
  I, N: Int32;
begin
  N := Length(AText);
  for I := 1 to N div 2 do
    if AText[I] <> AText[N - I + 1] then
      Exit(False);
  Exit(True);
end function;

// ---- testes ----

var
  GBase: Int32;

// [Setup] roda antes de CADA teste (há também [Teardown], [OneTimeSetup] e [OneTimeTeardown])
[Setup] procedure Prepare;
begin
  GBase := 10;
end procedure;

[Test] procedure DivideWorks;
begin
  Assert.AreEqual(5, Divide(GBase, 2));
  Assert.AreEqual(3, Divide(10, 3), 'divisão inteira');
end procedure;

// um teste que espera uma exceção
[Test] procedure DivideByZeroRaises;
begin
  Assert.Throws(Exception, procedure; begin Divide(1, 0); end);
end procedure;

// [TestCase] repete o mesmo teste com várias linhas de dados
[TestCase(2, 3, 5)] [TestCase(10, 20, 30)] [TestCase(-1, 1, 0)]
procedure SumCases(const A, B, AExpected: Int32);
begin
  Assert.AreEqual(AExpected, A + B);
end procedure;

[Test] procedure PalindromeWorks;
begin
  Assert.IsTrue(IsPalindrome('arara'));
  Assert.IsTrue(not IsPalindrome('fenner'));
end procedure;

// falha DE PROPÓSITO, para você ver como o relatório mostra o esperado e o obtido
[Test] procedure WrongOnPurpose;
begin
  Assert.AreEqual(7, Divide(GBase, 2), 'o resultado certo é 5');
end procedure;

[Test] [Ignore('ainda não implementado')] procedure Later;
begin
end procedure;

type
  TMain = class(TTestSuite) end class;         // o meta module do compilador gera TMain.RunAll

begin
  TTestRunner.SetNoTime(True);           // sem tempos no relatório: saída reproduzível
  // O código de saída é 0 quando tudo passa e 1 quando algum teste falha.
  Halt(TMain.Create.RunAll);
end.
Tests.config
Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\testing
Saída
[ OK ] Tests.DivideWorks
[ OK ] Tests.DivideByZeroRaises
[ OK ] Tests.SumCases(2, 3, 5)
[ OK ] Tests.SumCases(10, 20, 30)
[ OK ] Tests.SumCases(-1, 1, 0)
[ OK ] Tests.PalindromeWorks
[FAIL] Tests.WrongOnPurpose
       o resultado certo é 5: esperado 7, obtido 5
       em Tests.fxp:67
[SKIP] Tests.Later (ainda não implementado)

8 teste(s): 6 passou(aram), 1 falhou(aram), 0 com erro, 1 ignorado(s)

Demo 57. Um teste falha de propósito (WrongOnPurpose) para mostrar o esperado e o obtido; por isso o programa sai com código 1.

Leia o relatório: [ OK ] passou, [FAIL] asserção falhou (com a mensagem e o arquivo:linha), [ERRO] exceção inesperada, [SKIP] ignorado. A linha final resume tudo. TTestRunner.SetNoTime(True) tira os tempos para a saída ser sempre igual.

Opções do executável#

terminal
Tests.exe --list                   # lista os testes sem rodar
Tests.exe --filter=Sum             # só os que contêm "Sum" no nome (substring)
Tests.exe --notime                 # sem tempos
Tests.exe --junit=r.xml            # relatório JUnit XML (testsuite, testcase, failure, error, skipped)

Num projeto criado por fc new --test, o comando fc test compila e roda a suíte; veja Projetos e fenner.toml.

Armadilhas#

  • O arquivo:linha do relatório é o da rotina de teste, não o do Assert que falhou (o compilador não dá a linha do chamador).
  • Dois [Test] homônimos em módulos diferentes colidem (a chamada não é qualificada).
  • A rotina precisa estar visível à suíte, e o fixture precisa de Create sem argumentos.
  • Compilar um programa de teste custa a mais (~8 s): um fc filho roda o meta module.
  • O tempo (ms) usa GetTickCount64 (Windows). Sem execução paralela dos testes.
  • Um atributo marcador se declara attribute Nome; (sem parênteses).

Veja também#