W jednym z ostatnich wpisów dotyczących Prologa, poruszyłem temat modularyzacji, omawiając go na przykładzie kilku implementacji tego języka. Okazało się, że o ile nowoczesne odmiany, takie jak SWI-Prolog radzą sobie na tym polu całkiem nieźle, o tyle starsze warianty, np. GNU Prolog, mają tu niestety pewne braki.
A jak wygląda w Prologu kwestia testów jednostkowych? Jakiś czas temu zetknąłem się z tym zagadnieniem osobiście, kiedy po napisaniu zestawu testów w SWI-PL, próbowałem przenieść je na grunt GNU Prologa. Okazało się to dość nietrywialne, tak że finalnie musiałem uciec się do zadania pytania na Stacku.
Spis treści
Testy w SWI-Prolog
Zacznijmy od przykładowej test suity, jaką możemy zaimplementować w SWI-Prologu. Załóżmy, że będziemy chcieli przetestować predykat fizzbuzz/2
, stanowiący rozwiązanie znanego zadania rekrutacyjnego:
1 2 3 4 |
fizzbuzz(X, "FizzBuzz") :- 0 is X mod 15, !. fizzbuzz(X, "Fizz") :- 0 is X mod 3, !. fizzbuzz(X, "Buzz") :- 0 is X mod 5, !. fizzbuzz(X, X). |
plunit
Najprostszym sposobem będzie skorzystanie ze standardowego modułu plunit:
1 2 3 4 5 6 |
:- begin_tests(fizzbuzz_tests). test(should_be_fizzbuzz) :- fizzbuzz(45, Result), assertion(Result == "FizzBuzz"). test(should_be_fizz) :- fizzbuzz(42, Result), assertion(Result == "Fizz"). test(should_be_buzz) :- fizzbuzz(95, Result), assertion(Result == "Buzz"). test(should_be_number) :- fizzbuzz(98, Result), assertion(Result == 98). :- end_tests(fizzbuzz_tests). |
Po załadowaniu modułu z testami (np. jeśli nazwaliśmy go tests.pl
, to w swipl
wystarczy że wpiszemy [tests].
), możemy uruchomić je, wpisując run_tests.
.
Aktualnie nasze testy pokrywają tylko kilka arbitralnie wybranych przez nas przypadków. Jeśli w identyczny sposób chcemy przetestować więcej wartości, często używamy do tego tzw. testów sparametryzowanych. Technikę tę możemy zastosować również w plunit
, używając predykatu forall/1
:
1 2 3 4 5 |
test( should_be_fizz, [forall(member(N, [3,6,9,12,18,21]))] ) :- fizzbuzz(N, Result), assertion(Result == "Fizz"). |
Listę wartości testowych moglibyśmy też wygenerować sobie przy pomocy predykatu between/3
, który wylicza przedział z podanego zakresu. Niestety nie pozwala on na zdefiniowanie kroku (czyli możemy wygenerować np. listę [1,2,3,4,5,6]
, ale już nie [1,3,5,7,9]
.
Na szczęście, dzięki jednemu z użytkowników Prologa, powstał moduł (dostępny na GitHubie), który zawiera m.in. predykat between/4
, rozwiązujący ten problem. Tym sposobem, możemy zaimplementować np. taki oto test:
1 2 3 4 5 |
test( should_be_fizzbuzz, [forall(between(15, 100, 15, N))] ) :- fizzbuzz(N, Result), assertion(Result == "FizzBuzz"). |
crisp
Alternatywnie, możemy skierować się ku modułowi o nazwie crisp
(GitHub), który reklamuje się jako narzędzie stawiające na maksymalną prostotę i łatwość użycia. Główną rolę odgrywa tu predykat describe/2
, który przyjmuje nazwę test suity (zwyczajowo stanowi ją nazwa testowanego predykatu) oraz listę przypadków testowych:
1 2 3 4 5 6 |
describe(fizzbuzz/2, [ fizzbuzz(3, "Fizz") , fizzbuzz(5, Result), assertion(Result == "Buzz") , fizzbuzz(10, 10) , fizzbuzz(15, "FizzBuzz") ]). |
Chociaż, tak jak widać w trzeciej linijce, wciąż możemy tu bez problemu używać predykatu assertion/1
, to niestety w przypadku failującego testu, nie otrzymamy już tak ładnie sformatowanego komunikatu jak w plunit.
Warto dodać, że crisp
(podobnie zresztą jak plunit
) oferuje też możliwość łatwego sprawdzenia przypadków, w których dany cel ma kończyć się niepowodzeniem oraz weryfikacji czy rezultat działania predykatu jest deterministyczny:
1 2 3 4 |
describe(fizzbuzz/2, [ onedet-fizzbuzz(5, "Buzz") , fail-fizzbuzz(4, "Fizz") ]). |
Przy naszej aktualnej implementacji predykatu fizzbuzz/2
, oba powyższe testy zakończą się sukcesem, gdyby jednak zmienić definicję:
1 |
fizzbuzz(X, "Buzz") :- 0 is X mod 5, !. |
na:
1 |
fizzbuzz(X, "Buzz") :- 0 is X mod 5. |
to pytając o fizzbuzz(5, "Buzz")
otrzymalibyśmy niejednoczną odpowiedź, przez co test rozpoczynający się od onedet-
zwróciłby błąd.
Testy w GNU Prolog
Jeśli do zaimplementowania naszego programu używamy GNU Prologa, to niestety kwestia testów automatycznych nieco się komplikuje. GNU Prolog nie posiada bowiem żadnego wbudowanego modułu służącego do tego celu. Co więcej, wiele spośród dostępnych dodatkowych bibliotek nie współpracuje z tą implementacją Prologa (np. wyżej wspomniany Crisp).
Kiedy z problemem tym udałem się na StackOverflow, pomocnej odpowiedzi udzielił mi… Paulo Moura, będący autorem języka programowania Logtalk. Zaproponował on użycie modułu lgtunit
dostępnego właśnie w Logtalku.
Dlaczego do testowania programów napisanych w GNU Prolog miałbym używać innego języka? – zapytasz. Otóż Logtalk jest językiem, który stanowi rozszerzenie Prologa – składnia jest praktycznie taka sama, ale dodatkowo mamy możliwość tworzenia kodu, który jest zorientowany obiektowo.
Logtalk do działania wymaga zainstalowanego normalnego kompilatora Prologa i współpracuje z różnymi jego implementacjami, włączając w to GNU Prolog. Ta funkcjonalność pozwala nam na stworzenie w Logtalku testów do modułów, które napisane są w czystym GNU Prologu. Oto jak wyglądać będą nasze testy przy wykorzystaniu lgtunit
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
:- include(fizzbuzz). :- object(fizzbuzz_tests, extends(lgtunit)). :- uses(lgtunit, [assertion/1]). :- uses(user, [fizzbuzz/2]). test(test_fizz, true(Result == "Fizz")) :- fizzbuzz(3, Result). test(test_buzz) :- fizzbuzz(20, Result), assertion(Result == "Buzz"). deterministic(test_deterministic_fizz) :- fizzbuzz(6, Result). fails(test_fizz_fail) :- fizzbuzz(3, "Buzz"). :- end_object. |
Dwa pierwsze testy pokazują możliwość użycia różnych „dialektów”, w zależności od osobistych preferencji, natomiast dwa kolejne to demonstracja funkcjonalności podobnych do tych, omówionych na przykładzie modułu crisp
. Dzięki predykatom takim jak deterministic
czy fails
zamiast test
, można w prosty sposób czy sprawdzany predykat jest deterministyczny lub czy failuje.
Pozostaje jeszcze tylko pytanie w jaki sposób w ogóle odpalić takie logtalkowo-gnuprologowe testy. Po poprawnym zainstalowaniu Logtalka, wystarczy że odpalimy jego konsolę i wykonamy następujące kroki:
- Zmień katalog na ten, w którym znajduje się plik z testami
- Załaduj moduł do testów jednostkowych, wpisując:
logtalk_load(lgtunit(tester)).
- Załaduj plik z testami:
logtalk_load(fizzbuzz_tests, [hook(lgtunit)]).
- Uruchom testy:
fizzbuzz_tests::run.