fpcunit/pl
│
English (en) │
français (fr) │
polski (pl) │
Przegląd
Fpcunit to framework do testowania modułów w stylu DUnit/JUnit/SUnit. Pozwala to na szybkie napisanie zestawu testów dla (logiki) kodu modułu (niekoniecznie takiego samego jak moduł Pascala, chociaż często tak jest).
Metodologie programowania, takie jak Test Driven Design, wykorzystują to, aby upewnić się, że najpierw kodujesz swoje oczekiwania/specyfikacje w testach jednostkowych, następnie piszesz główny kod, a następnie uruchamiasz testy i ulepszasz kod, aż wszystkie testy zakończą się pomyślnie.
fpcunit pozwala nie tylko na wizualną kontrolę przebiegów testów, ale także na systematyczne zbieranie wyników (za pomocą danych wyjściowych XML) i wykorzystanie ich do porównywania wersji m.in. błędy regresji (tzn. uruchamiasz testy regresji przy użyciu danych wyjściowych testu jednostkowego).
Zrzut ekranu programu uruchamiającego test GUI:
Powyższy obraz pokazuje, że na 10 testów, 6 testów zakończyło się niepowodzeniem. Wyjątki EAssertionFailure wskazują, że twierdzenia testowe (patrz poniżej) nie zostały spełnione – tj. test nie powiódł się. Powiązane komunikaty wskazują wynik oczekiwany przez test i wynik rzeczywisty osiągnięty przez test.
Użycie w FPC/Lazarus
Testy FPCUnit są używane w ramach testowej bazy danych FPC: Databases#Running_FPC_database_tests
Istnieją również testy dla pakietów kompilatora/rdzenia FPC, ale przypuszczalnie poprzedzają one fpcunit i wykorzystują prostsze podejście.
Użycie
Do skonfigurowania Twojego nowego projektu testowego, najłatwiej jest użyć Lazarusa. Poniżej znajduje się kilka opisów procedur/metod, których należy użyć w takim celu.
Metoda SetUp
Ta procedura jest obecna we wszystkich testach FPCUnit. Konfiguruje środowisko testowe przed uruchomieniem każdego testu – innymi słowy nie tylko przed i po uruchomieniu całego zestawu testów, ale dla każdego testu”. Możesz to wykorzystać m.in. do tego, aby wypełnić bazę danych danymi testowymi.
Metoda TearDown
Ta procedura jest obecna we wszystkich testach FPCUnit i jest odwrotnością metody SetUp. Czyści środowisko testowe po każdym uruchomieniu testu. Możesz to wykorzystać m.in., aby wyczyść dane testowe z bazy danych.
Dekorator testów: OneTimeSetUp i OneTimeTearDown
Wymienione powyżej procedury SetUp i TearDown są uruchamiane jednorazowo w każdym teście. Możesz także uruchamiać te procedury raz na jedną instancję/wykonywanie każdego przebiegu testowego.
W tym celu użyj OneTimeSetUp i OneTimeTearDown w klasie dziedziczącej po „dekoratorze testów” TTestSetup i zarejestruj ją, np.:
uses
...
testdecorator
...
TDBBasicsTestSetup = class(TTestSetup)
protected
procedure OneTimeSetUp; override;
procedure OneTimeTearDown; override;
end;
...
initialization
// upewnij się, że zarejestrowałeś swój test wraz z dekoratorem, aby wiedział, jak uruchamiać SetUp/TearDowns
RegisterTestDecorator(TDBBasicsTestSetup, TTestDBBasics);
Testy
Piszesz własne testy jako procedury w sekcji published (opublikowane) swojej klasy testowej (w sekcji private, protected lub public nie będą działać). Możesz użyć AssertEquals itp., aby określić, co powinno być testowane i dać odpowiedni komunikat, gdy test się nie powiedzie.
Jeśli chcesz „oblać test”, możesz m.in. posłużyć się tym:
if 5=0 then //śmieszny przykład, ale rozumiesz, o co mi chodzi. Możesz użyć innych procedur Assert*, aby znacznie łatwiej przetestować równość itp.
AssertTrue('Ta część kodu nigdy nie powinna zostać wykonana w tym teście.',false);
Jeśli test się nie powiedzie, zostanie zgłoszony wyjątek EAssertionFailedError z komunikatem określonym w Assert*. W ten sposób możesz dodać serię podtestów i stwierdzić, który podtest się nie powiódł. Uwaga: program uruchamiający testy zatrzyma się po pierwszym niepowodzeniu asercji, więc kolejne podtesty nie będą wykonywane. Jeśli chcesz zawsze testować wszystko, podziel te podtesty na osobne procedury testowe.
Zamiast procedur Assert* można również użyć procedur Check* kompatybilnych z DUnit (np. CheckEquals), które dają bardziej opisowe komunikaty o błędach w wynikach testu: zawierają wartości oczekiwane i rzeczywiste.
Kolejność, w jakiej testy są uruchamiane, to kolejność, w jakiej pojawiają się one w definicji klasy testowej.
Przykładowy test
Ttestexport1 = class(Ttestcase)
...
published
procedure TestOutput;
...
procedure Ttestexport1.TestOutput;
const
OutputFilename='output.csv';
begin
TestDataSet.Close;
if FileExists(OutputFilename) then DeleteFile(OutputFileName);
TestDataset.FileName:=OutputFileName;
TestDataset.Open;
// Wypełnij dane testowe
TestDataset.Append;
TestDataset.FieldByName('ID').AsInteger := 1;
// Dane z cudzysłowami
TestDataset.FieldByName('NAME').AsString := 'J"T"';
TestDataset.FieldByName('BIRTHDAY').AsDateTime := ScanDateTime('yyyymmdd', '19761231', 1);
TestDataset.Post;
TestDataset.Last;
TestDataset.First;
TestDataset.First;
AssertEquals('Liczba rekordów w testowym dataset', 1, TestDataset.RecordCount);
TestDataset.Close;
end;
Hierarchia testów
W prostych przypadkach Ty (lub Lazarus) rejestrujesz wszystkie swoje testy za pomocą wywołań takich jak:
uses
...
testregistry
...
initialization
RegisterTest(Ttestexport1); //przekazujesz nazwę klasy, aby zarejestrować ją do uruchomienia
Jednak możesz także utworzyć wiele warstw, aby pogrupować testy, gdy Twój projekt stanie się duży:
initialization
RegisterTest('WorldDominationApp.ExportCheesePlan',Ttestexport1); //Poziomy są oddzielone kropkami
RegisterTest('WorldDominationApp.Obsolete',TtestPinkysBrain1); //inna kategoria
Niestandardowe nazwy testów
Domyślnie nazwa Twojej klasy potomnej od TTestCase jest używana jako nazwa zestawu testów, a nazwy metod są używane jako nazwy przypadków testowych. Możesz także przypisać własne nazwy, np. jeśli masz klasę, która przeprowadzi różne testy w zależności od innych ustawień.
Poniższy kod utworzy test o nazwie MyTestName (zastępując nazwę metody) w MyTestSuiteName (zastępując nazwę klasy).
interface
type
TMyTestClass = class(TTestCase)
protected
// zastąp domyślną obsługę testów, uruchamiając opublikowaną metodę
procedure RunTest; override;
// nie potrzebujesz tutaj żadnej opublikowanej metody
end;
implementation
procedure TMyTestClass.RunTest;
begin
// nie wywołuj metody dziedziczonej.
// tutaj będzie logika twojego testu ...
AssertTrue(False);
end;
initialization
RegisterTest('MyTestSuiteName', TMyTestClass.CreateWithName('MyTestName');
end.
Automatyczna modyfikacja pól
Jeśli dodasz jakiekolwiek pole do podklasy TTestCase, powinieneś mieć świadomość, że te pola zostaną zresetowane do wartości domyślnych przed rozpoczęciem testu. Nie uważam tego za błąd, ponieważ wyniki testów powinny być niezależne: wynik testu nie powinien zależeć od wyniku żadnego innego testu. Odtworzyłem to zachowanie zarówno w systemie Windows, jak i Linux Mint XFCE.
Jeśli naprawdę potrzebujesz wiarygodnej wartości zmiennej, użycie class var (zmiennej klasy) zamiast pól, rozwiąże problem.
Podczas dalszych testów tego modułu odkryłem, że w poniższym kodzie, jeśli zastąpię fx:double; przez class var fx:double; test już się nie powiedzie! Zadeklarowanie jednej zmiennej jako zmiennej klasowej rozwiązało problem. To sprawia, że wierzę, że jest to błąd.
Zwykle błędy nie są zapisywane w dokumentacji, ale ten kosztował nas wiele godzin straconego czasu podczas testów jednostkowych. Ponadto przed zgłoszeniem jakiegokolwiek błędu należy jasno określić oczekiwane zachowanie. Ta strona wiki musi opisywać oczekiwane zachowanie. Czy błąd polega na tym, że pola są przypisane do ich domyślnych wartości 0, czy też błąd polega na tym, że po dodaniu klasy var nie są już przypisane do 0? Zgodnie z tym odniesieniem https://sergworks.wordpress.com/2012/08/31/introduction-to-unit-testing-with-lazarus/, błąd polegał na tym, że zostały one wyczyszczone do wartości domyślnych.
unit TestCase1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testutils, testregistry, Dialogs;
type
TTestCase1= class(TTestCase)
protected
fx:double;
fi:integer;
procedure SetUp; override;
procedure TearDown; override;
published
procedure One;
procedure Two;
end;
implementation
procedure TTestCase1.One;
begin
fx:=0.5;
fi:=3;
IF fi <> 3 THEN
Fail('Musi być 3.');
end;
procedure TTestCase1.Two;
begin
ShowMessage('fx='+fx.ToString);
ShowMessage('fi='+fi.ToString);
IF fi <> 3 THEN //Nie powiedzie się, fi =0, zostało automatycznie wyczyszczone, nawet jeśli test 1 był już przeprowadzany
Fail('Musi być 3.');
end;
procedure TTestCase1.SetUp;
begin
end;
procedure TTestCase1.TearDown;
begin
end;
initialization
RegisterTest(TTestCase1);
end.
Wyjście
Program uruchamiający testy konsoli może wyprowadzać dane wyjściowe w formacie XML (oryginalnym formacie FPCUnit lub bardziej zaawansowanym formacie podobnym do DUnit2, jeśli używasz modułu xmltestreport), w formacie zwykłego tekstu i latex (np. nadającego się do eksportu PDF). Program uruchamiający testy GUI w razie potrzeby generuje dane wyjściowe w formacie XML (przy użyciu tego samego formatu XML xmltestreport).
Dostosowywanie wyjścia
Możesz użyć własnego „obserwatora”, który śledzi wyników testu i wyprowadza dane testowe w dowolny sposób. Utwórz T*Listener, który implementuje interfejs ITestListener. To tylko 5 wymaganych metod do wdrożenia.
W aplikacji uruchamiającej testy (np. kopii fpctestconsole), dodaj obiekt obserwatora; zarejestruj tego testowego obserwatora w środowisku testowym (np. w programie uruchamiającym testy konsoli) za pomocą wywołania TestResult.AddListener(), a wyniki testów będą do niego dostarczane w miarę ich pojawiania się.
Testdbwriter
Przykładem niestandardowego obserwatora jest program do zapisywania danych wyjściowych bazy danych dostępny pod adresem https://bitbucket.org/reiniero/testdbwriter. Ten program zapisze wszystkie wyniki testów w bazie danych, która jest zoptymalizowana pod kątem otrzymywania dużych ilości wyników testów (przydatne do używania na serwerze CI, takim jak Jenkins lub do importu/konsolidacji wyników testów). Wspomniane repozytorium zawiera przykład, który uruchamia wyniki testów frameworka db do (innej) bazy danych.
ToDo: dostosuj to; użyj nowego modułu xml
Przykładem dostępnego dodatkowego obserwatora jest TXMLResultsWriter w module xmlreporter w <fpc>\packages\fcl-fpcunit\src\xmlreporter.pas.
todo: w rzeczywistości dbtestframework wydaje się używać starej metody wyjściowej xml... Przykład dostosowanego programu uruchamiającego testy, który używa dodatkowego obserwatora, można znaleźć w <fpc>\packages\fcl-db\tests\dbtestframework.pas, który zawiera ten kod do wyprowadzenia do niestandardowych obserwatorów (program zapisujący XML i zapisujący skrót, który umieszcza wyjście w archiwum .tar, przydatne do przetwarzania zdalnego):
uses
//...pozostałe moduły potrzebne do testowania...
fpcunit,...
// moduły z TXMLResultsWriter i TDigestResultsWriter
testreport,DigestTestReport
...
Procedure LegacyOutput;
var
FXMLResultsWriter: TXMLResultsWriter;
FDigestResultsWriter: TDigestResultsWriter;
testResult: TTestResult;
begin
testResult := TTestResult.Create;
FXMLResultsWriter := TXMLResultsWriter.Create;
FDigestResultsWriter := TDigestResultsWriter.Create(nil);
try
testResult.AddListener(FXMLResultsWriter);
testResult.AddListener(FDigestResultsWriter);
// Ustaw niektóre właściwości specyficzne dla tego sposobu zapisywania wyników:
FDigestResultsWriter.Comment:=dbtype;
FDigestResultsWriter.Category:='DB';
FDigestResultsWriter.RelSrcDir:='fcl-db';
//WriteHeader jest specyficzny dla tego obserwatora; zapisuje nagłówek do pliku XML
//zauważ, że nie jest wywoływany dla FDigestResultsWriter
FXMLResultsWriter.WriteHeader;
// To wykonuje rzeczywisty przebieg testowy, a dane wyjściowe zostaną przetworzone przez obserwatorów:
GetTestRegistry.Run(testResult);
// Podobnie WriteResult jest specyficzny dla tego obserwatora; zapisuje on
FXMLResultsWriter.WriteResult(testResult);
finally
testResult.Free;
FXMLResultsWriter.Free;
FDigestResultsWriter.Free;
end;
end;
Alternatywy
- DUnit2 — ogromna poprawa w stosunku do oryginalnego DUnit. Pierwotnie napisany tylko dla Delphi i używany przez ogromny zestaw testowy frameworka tiOPF.
- FPTest — rozwidlenie DUnit2, które jest dostrojone specjalnie do użytku z kompilatorem Free Pascal.
Lazarus
Lazarus posiada moduły uruchamiające testy consoletestrunner i GUI, które można zainstalować, instalując pakiet FPCUnitTestRunner. Pomoże to w tworzeniu i uruchamianiu testów jednostkowych za pomocą GUI (lub konsoli, jeśli chcesz).
Consoletestrunner jest kompatybilny z FPC, więc nie potrzebujesz Lazarusa, aby go skompilować. Wersja Lazarusa różni się nieco od tej w FPC (np. użycie wyjścia UTF8 itp.).
Program GUI jest łatwiejszy w użyciu.
W programie GUI, jeśli chcesz uruchomić wszystkie testy, obecnie musisz najpierw kliknąć element testu, zanim zostanie aktywowany przycisk Run all tests („Uruchom wszystkie testy”).
GDB błędy/funkcje
Uwaga (wrzesień 2012): błąd/nieudokumentowana funkcja w debuggerze używanym przez Lazarus/FPC (gdb) oznacza, że przekazanie --all jako parametru uruchomienia nie daje żadnego efektu. Przekazanie tego parametru może być przydatne, gdy debugowanie programów uruchamiających testy fpcunit w konsoli nie przynosi żadnego efektu. Obejście: użyj -a.
Zobacz błąd [1]
Zobacz także
- https://www.freepascal.org/~michael/articles/fpcunit/fpcunit.pdf Obejmuje użycie zarówno FPC, jak i Lazarusa. Wyjaśnia różne zawiłości klasy. Integracja fragmentów tego pliku PDF z tą dokumentacją (lub odwrotnie) może być bardzo pomocna.
- http://sergworks.wordpress.com/2012/08/31/introduction-to-unit-testing-with-lazarus/ Świetny artykuł o tym, jak uruchomić FPCUnit w zaciszu swojego Lazarus IDE.
- http://www.pp4s.co.uk/main/tu-testing-auto2.html Przykładowy program, który demonstruje testowanie FPCUnit
http://www.win.tue.nl/~mousavi/testing/4.pdfPrezentacja o FPCUnit