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.
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#
| Atributo | Efeito |
|---|---|
[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].
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.Platform MSWin64
Default Platform MSWin64
Mode Debug
Default Mode Debug
PlatformMode All * *
PlatformMode MSWin64Debug MSWin64 Debug
Config All SourcePath ($FcPath)\..\testing[ 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#
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:linhado relatório é o da rotina de teste, não o doAssertque 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
Createsem argumentos. - Compilar um programa de teste custa a mais (~8 s): um
fcfilho 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#
- Cobertura de código, para medir o que os testes exercitam.
- Meta modules e atributos.
- Demo
57-testes.