fpcunit/pl: what changed
Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.
Title: fpcunit/pl
| Old | New | Line | |
|---|---|---|---|
| 1 | + | ## Przegląd | |
| 2 | + | ||
| 3 | + | Fpcunit to framework do testowania modułów w stylu DUnit/JUnit/[SUnit](https://en.wikipedia.org/wiki/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). | |
| 4 | + | ||
| 5 | + | 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. | |
| 6 | + | ||
| 7 | + | 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). | |
| 8 | + | ||
| 9 | + | Zrzut ekranu programu uruchamiającego test GUI: | |
| 10 | + | ||
| 11 | + |  | |
| 12 | + | ||
| 13 | + | 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. | |
| 14 | + | ||
| 15 | + | ## Użycie w FPC/Lazarus | |
| 16 | + | ||
| 17 | + | Testy FPCUnit są używane w ramach testowej bazy danych FPC: [[Databases|Databases#Running_FPC_database_tests]] | |
| 18 | + | ||
| 19 | + | Istnieją również testy dla pakietów kompilatora/rdzenia FPC, ale przypuszczalnie poprzedzają one fpcunit i wykorzystują prostsze podejście. | |
| 20 | + | ||
| 21 | + | ## Użycie | |
| 22 | + | ||
| 23 | + | 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. | |
| 24 | + | ||
| 25 | + | ### Metoda SetUp | |
| 26 | + | ||
| 27 | + | 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. | |
| 28 | + | ||
| 29 | + | ### Metoda TearDown | |
| 30 | + | ||
| 31 | + | 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. | |
| 32 | + | ||
| 33 | + | ### Dekorator testów: OneTimeSetUp i OneTimeTearDown | |
| 34 | + | ||
| 35 | + | 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. | |
| 36 | + | ||
| 37 | + | W tym celu użyj OneTimeSetUp i OneTimeTearDown w klasie dziedziczącej po „dekoratorze testów” TTestSetup i zarejestruj ją, np.: | |
| 38 | + | ||
| 39 | + | ``` pascal | |
| 40 | + | uses | |
| 41 | + | ... | |
| 42 | + | testdecorator | |
| 43 | + | ... | |
| 44 | + | TDBBasicsTestSetup = class(TTestSetup) | |
| 45 | + | protected | |
| 46 | + | procedure OneTimeSetUp; override; | |
| 47 | + | procedure OneTimeTearDown; override; | |
| 48 | + | end; | |
| 49 | + | ... | |
| 50 | + | initialization | |
| 51 | + | // upewnij się, że zarejestrowałeś swój test wraz z dekoratorem, aby wiedział, jak uruchamiać SetUp/TearDowns | |
| 52 | + | RegisterTestDecorator(TDBBasicsTestSetup, TTestDBBasics); | |
| 53 | + | ``` | |
| 54 | + | ||
| 55 | + | ### Testy | |
| 56 | + | ||
| 57 | + | 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. | |
| 58 | + | ||
| 59 | + | Jeśli chcesz „oblać test”, możesz m.in. posłużyć się tym: | |
| 60 | + | ||
| 61 | + | ``` pascal | |
| 62 | + | 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. | |
| 63 | + | AssertTrue('Ta część kodu nigdy nie powinna zostać wykonana w tym teście.',false); | |
| 64 | + | ``` | |
| 65 | + | ||
| 66 | + | 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. | |
| 67 | + | ||
| 68 | + | 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. | |
| 69 | + | ||
| 70 | + | Kolejność, w jakiej testy są uruchamiane, to kolejność, w jakiej pojawiają się one w definicji klasy testowej. | |
| 71 | + | ||
| 72 | + | #### Przykładowy test | |
| 73 | + | ||
| 74 | + | ``` pascal | |
| 75 | + | Ttestexport1 = class(Ttestcase) | |
| 76 | + | ... | |
| 77 | + | published | |
| 78 | + | procedure TestOutput; | |
| 79 | + | ... | |
| 80 | + | procedure Ttestexport1.TestOutput; | |
| 81 | + | const | |
| 82 | + | OutputFilename='output.csv'; | |
| 83 | + | begin | |
| 84 | + | TestDataSet.Close; | |
| 85 | + | ||
| 86 | + | if FileExists(OutputFilename) then DeleteFile(OutputFileName); | |
| 87 | + | TestDataset.FileName:=OutputFileName; | |
| 88 | + | TestDataset.Open; | |
| 89 | + | // Wypełnij dane testowe | |
| 90 | + | TestDataset.Append; | |
| 91 | + | TestDataset.FieldByName('ID').AsInteger := 1; | |
| 92 | + | // Dane z cudzysłowami | |
| 93 | + | TestDataset.FieldByName('NAME').AsString := 'J"T"'; | |
| 94 | + | TestDataset.FieldByName('BIRTHDAY').AsDateTime := ScanDateTime('yyyymmdd', '19761231', 1); | |
| 95 | + | TestDataset.Post; | |
| 96 | + | ||
| 97 | + | TestDataset.Last; | |
| 98 | + | TestDataset.First; | |
| 99 | + | TestDataset.First; | |
| 100 | + | AssertEquals('Liczba rekordów w testowym dataset', 1, TestDataset.RecordCount); | |
| 101 | + | TestDataset.Close; | |
| 102 | + | end; | |
| 103 | + | ``` | |
| 104 | + | ||
| 105 | + | #### Hierarchia testów | |
| 106 | + | ||
| 107 | + | W prostych przypadkach Ty (lub Lazarus) rejestrujesz wszystkie swoje testy za pomocą wywołań takich jak: | |
| 108 | + | ||
| 109 | + | ``` pascal | |
| 110 | + | uses | |
| 111 | + | ... | |
| 112 | + | testregistry | |
| 113 | + | ... | |
| 114 | + | initialization | |
| 115 | + | RegisterTest(Ttestexport1); //przekazujesz nazwę klasy, aby zarejestrować ją do uruchomienia | |
| 116 | + | ``` | |
| 117 | + | ||
| 118 | + | Jednak możesz także utworzyć wiele warstw, aby pogrupować testy, gdy Twój projekt stanie się duży: | |
| 119 | + | ||
| 120 | + | ``` pascal | |
| 121 | + | initialization | |
| 122 | + | RegisterTest('WorldDominationApp.ExportCheesePlan',Ttestexport1); //Poziomy są oddzielone kropkami | |
| 123 | + | RegisterTest('WorldDominationApp.Obsolete',TtestPinkysBrain1); //inna kategoria | |
| 124 | + | ``` | |
| 125 | + | ||
| 126 | + | #### Niestandardowe nazwy testów | |
| 127 | + | ||
| 128 | + | 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ń. | |
| 129 | + | ||
| 130 | + | Poniższy kod utworzy test o nazwie MyTestName (zastępując nazwę metody) w MyTestSuiteName (zastępując nazwę klasy). | |
| 131 | + | ||
| 132 | + | ``` pascal | |
| 133 | + | interface | |
| 134 | + | ||
| 135 | + | type | |
| 136 | + | TMyTestClass = class(TTestCase) | |
| 137 | + | protected | |
| 138 | + | // zastąp domyślną obsługę testów, uruchamiając opublikowaną metodę | |
| 139 | + | procedure RunTest; override; | |
| 140 | + | // nie potrzebujesz tutaj żadnej opublikowanej metody | |
| 141 | + | end; | |
| 142 | + | ||
| 143 | + | implementation | |
| 144 | + | ||
| 145 | + | procedure TMyTestClass.RunTest; | |
| 146 | + | begin | |
| 147 | + | // nie wywołuj metody dziedziczonej. | |
| 148 | + | // tutaj będzie logika twojego testu ... | |
| 149 | + | AssertTrue(False); | |
| 150 | + | end; | |
| 151 | + | ||
| 152 | + | initialization | |
| 153 | + | RegisterTest('MyTestSuiteName', TMyTestClass.CreateWithName('MyTestName'); | |
| 154 | + | end. | |
| 155 | + | ``` | |
| 156 | + | ||
| 157 | + | ## Automatyczna modyfikacja pól | |
| 158 | + | ||
| 159 | + | 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. | |
| 160 | + | ||
| 161 | + | Jeśli naprawdę potrzebujesz wiarygodnej wartości zmiennej, użycie *class var* (zmiennej klasy) zamiast pól, rozwiąże problem. | |
| 162 | + | ||
| 163 | + | 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. | |
| 164 | + | ||
| 165 | + | 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. | |
| 166 | + | ||
| 167 | + | ``` pascal | |
| 168 | + | unit TestCase1; | |
| 169 | + | ||
| 170 | + | {$mode objfpc}{$H+} | |
| 171 | + | ||
| 172 | + | interface | |
| 173 | + | ||
| 174 | + | uses | |
| 175 | + | Classes, SysUtils, fpcunit, testutils, testregistry, Dialogs; | |
| 176 | + | ||
| 177 | + | type | |
| 178 | + | ||
| 179 | + | TTestCase1= class(TTestCase) | |
| 180 | + | protected | |
| 181 | + | fx:double; | |
| 182 | + | fi:integer; | |
| 183 | + | procedure SetUp; override; | |
| 184 | + | procedure TearDown; override; | |
| 185 | + | published | |
| 186 | + | procedure One; | |
| 187 | + | procedure Two; | |
| 188 | + | end; | |
| 189 | + | ||
| 190 | + | implementation | |
| 191 | + | ||
| 192 | + | procedure TTestCase1.One; | |
| 193 | + | begin | |
| 194 | + | fx:=0.5; | |
| 195 | + | fi:=3; | |
| 196 | + | IF fi <> 3 THEN | |
| 197 | + | Fail('Musi być 3.'); | |
| 198 | + | end; | |
| 199 | + | procedure TTestCase1.Two; | |
| 200 | + | begin | |
| 201 | + | ShowMessage('fx='+fx.ToString); | |
| 202 | + | ShowMessage('fi='+fi.ToString); | |
| 203 | + | ||
| 204 | + | IF fi <> 3 THEN //Nie powiedzie się, fi =0, zostało automatycznie wyczyszczone, nawet jeśli test 1 był już przeprowadzany | |
| 205 | + | Fail('Musi być 3.'); | |
| 206 | + | end; | |
| 207 | + | ||
| 208 | + | procedure TTestCase1.SetUp; | |
| 209 | + | begin | |
| 210 | + | ||
| 211 | + | end; | |
| 212 | + | ||
| 213 | + | procedure TTestCase1.TearDown; | |
| 214 | + | begin | |
| 215 | + | ||
| 216 | + | end; | |
| 217 | + | ||
| 218 | + | initialization | |
| 219 | + | ||
| 220 | + | RegisterTest(TTestCase1); | |
| 221 | + | end. | |
| 222 | + | ``` | |
| 223 | + | ||
| 224 | + | ## Wyjście | |
| 225 | + | ||
| 226 | + | 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*). | |
| 227 | + | ||
| 228 | + | ### Dostosowywanie wyjścia | |
| 229 | + | ||
| 230 | + | 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. | |
| 231 | + | ||
| 232 | + | 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ę. | |
| 233 | + | ||
| 234 | + | #### Testdbwriter | |
| 235 | + | ||
| 236 | + | 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. | |
| 237 | + | ||
| 238 | + | #### ToDo: dostosuj to; użyj nowego modułu xml | |
| 239 | + | ||
| 240 | + | Przykładem dostępnego dodatkowego obserwatora jest TXMLResultsWriter w module xmlreporter w \<fpc\>\packages\fcl-fpcunit\src\xmlreporter.pas. | |
| 241 | + | ||
| 242 | + | *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): | |
| 243 | + | ||
| 244 | + | ``` pascal | |
| 245 | + | uses | |
| 246 | + | //...pozostałe moduły potrzebne do testowania... | |
| 247 | + | fpcunit,... | |
| 248 | + | // moduły z TXMLResultsWriter i TDigestResultsWriter | |
| 249 | + | testreport,DigestTestReport | |
| 250 | + | ... | |
| 251 | + | Procedure LegacyOutput; | |
| 252 | + | ||
| 253 | + | var | |
| 254 | + | FXMLResultsWriter: TXMLResultsWriter; | |
| 255 | + | FDigestResultsWriter: TDigestResultsWriter; | |
| 256 | + | testResult: TTestResult; | |
| 257 | + | ||
| 258 | + | begin | |
| 259 | + | testResult := TTestResult.Create; | |
| 260 | + | FXMLResultsWriter := TXMLResultsWriter.Create; | |
| 261 | + | FDigestResultsWriter := TDigestResultsWriter.Create(nil); | |
| 262 | + | try | |
| 263 | + | testResult.AddListener(FXMLResultsWriter); | |
| 264 | + | testResult.AddListener(FDigestResultsWriter); | |
| 265 | + | // Ustaw niektóre właściwości specyficzne dla tego sposobu zapisywania wyników: | |
| 266 | + | FDigestResultsWriter.Comment:=dbtype; | |
| 267 | + | FDigestResultsWriter.Category:='DB'; | |
| 268 | + | FDigestResultsWriter.RelSrcDir:='fcl-db'; | |
| 269 | + | //WriteHeader jest specyficzny dla tego obserwatora; zapisuje nagłówek do pliku XML | |
| 270 | + | //zauważ, że nie jest wywoływany dla FDigestResultsWriter | |
| 271 | + | FXMLResultsWriter.WriteHeader; | |
| 272 | + | // To wykonuje rzeczywisty przebieg testowy, a dane wyjściowe zostaną przetworzone przez obserwatorów: | |
| 273 | + | GetTestRegistry.Run(testResult); | |
| 274 | + | // Podobnie WriteResult jest specyficzny dla tego obserwatora; zapisuje on | |
| 275 | + | FXMLResultsWriter.WriteResult(testResult); | |
| 276 | + | finally | |
| 277 | + | testResult.Free; | |
| 278 | + | FXMLResultsWriter.Free; | |
| 279 | + | FDigestResultsWriter.Free; | |
| 280 | + | end; | |
| 281 | + | end; | |
| 282 | + | ``` | |
| 283 | + | ||
| 284 | + | ## Alternatywy | |
| 285 | + | ||
| 286 | + | - [DUnit2](http://www.sourceforge.net/projects/dunit2/) — ogromna poprawa w stosunku do oryginalnego DUnit. Pierwotnie napisany tylko dla Delphi i używany przez ogromny zestaw testowy frameworka [[tiOPF]]. | |
| 287 | + | ||
| 288 | + | - [[FPTest/pl|FPTest]] — rozwidlenie DUnit2, które jest dostrojone specjalnie do użytku z kompilatorem Free Pascal. | |
| 289 | + | ||
| 290 | + | ## Lazarus | |
| 291 | + | ||
| 292 | + | 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). | |
| 293 | + | ||
| 294 | + | 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.). | |
| 295 | + | ||
| 296 | + | Program GUI jest łatwiejszy w użyciu. | |
| 297 | + | ||
| 298 | + | 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”). | |
| 299 | + | ||
| 300 | + | ## GDB błędy/funkcje | |
| 301 | + | ||
| 302 | + | **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. | |
| 303 | + | ||
| 304 | + | Zobacz błąd [1](http://bugs.freepascal.org/view.php?id=22893) | |
| 305 | + | ||
| 306 | + | ## Zobacz także | |
| 307 | + | ||
| 308 | + | - <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. | |
| 309 | + | - <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. | |
| 310 | + | - <http://www.pp4s.co.uk/main/tu-testing-auto2.html> Przykładowy program, który demonstruje testowanie FPCUnit | |
| 311 | + | - ~~<http://www.win.tue.nl/~mousavi/testing/4.pdf>~~ Prezentacja o FPCUnit | |
| 312 | + | ||
| 313 | + | --- | |
| 314 | + | *From the Free Pascal wiki, page "fpcunit/pl", as it stood on 2024-05-25 after a change by Slawek, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).* |
