Setting Up Unit Testing In SWI-Prolog

BEFORE TESTING, let’s structure our project. For this example we’ll diagnose an apple to see if it is good to eat. First we’ll setup our file structure, then we’ll add the content, and run the tests.

Get Organised

A STANDARD structure is to separate your knowledge base from your reasoning over it. This makes it easier to interchange your knowledge base. We’ll also keep our tests in their own file for the sake of readability.

Setup the following file structure:

project/
|-- kb.pl
|-- inference.pl
|-- inference.plt

kb.pl holds the facts, data, knowledge, whatever you wish to call it. There’s no reasoning done in that file. inference.pl is our placeholder name for where you do your fancy work, reasoning, etc. inference.plt is the test file, it must have the same name as what you’re testing for SWI-Prolog to automatically find it and load it. The only difference is the “t” on the end.

LET’S ADD some Prolog! Starting with kb.pl, here’s some rather glib information about apples. ako stands for “a kind of”, isa is the “is a” relationship, i.e. an instance of some class. We have some information on what an apple is, and a few different kinds of apples. We then have three instances of those apples, boringly called test_subjectN. To make thing’s fun, we also have a maggot called Maud, who’s living in one of the apples.

% kb.pl
:- module(kb, [fact/3]).

fact(fruit, ako, food).
fact(apple, ako, fruit).
fact(cox, ako, apple).
fact(braeburn, ako, apple).
fact(bramley, ako, apple).
fact(granny_smith, ako, apple).

fact(test_subject1, isa, bramley).
fact(test_subject1, rotten, no).
fact(test_subject1, mouldy, no).

fact(test_subject2, isa, bramley).
fact(test_subject2, rotten, yes).
fact(test_subject2, mouldy, no).

fact(test_subject3, isa, granny_smith).
fact(test_subject3, rotten, no).
fact(test_subject3, mouldy, no).
fact(maggot, ako, grub).
fact(maud, isa, maggot).
fact(maud, located_in, test_subject3).

Now let’s add some Prolog to inference.pl, this should define ako as a transitive relationship and make isa smart too. From there I’ve just created some quick tests to determine if an apple is edible.

% inference.pl
:- use_module(kb).

ako(Sub, Class) :- fact(Sub, ako, Class).
ako(Sub, Class) :- fact(Sub, ako, SC), ako(SC, Class).

isa(Ins, Class) :- fact(Ins, isa, Class).
isa(Ins, Class) :- fact(Ins, isa, C), ako(C, Class).

rotten(Item) :- fact(Item, rotten, yes), !.
mouldy(Item) :- fact(Item, mouldy, yes), !.
gone_bad(Item) :- rotten(Item), ! ; mouldy(Item).

infested(Item) :-
    isa(Grub, grub), fact(Grub, located_in, Item), !.

edible(Item) :-
    isa(Item, food),
    \+ gone_bad(Item),
    \+ infested(Item), !.

This would be the file you’d run and query. Let’s write some unit tests for it. Put this into inference.plt:

% inference.plt
:- begin_tests(inference).  % for plunit

:- include(inference).  % include for everything in local namespace

% add option [nondet] because a choice point is expected
test(ako_fact, [nondet]) :- ako(cox, apple).
test(ako_transitive, [nondet]) :- ako(cox, fruit), ako(cox, food).
% add option [fail] because this should fail
test(ako_fail, [fail]) :- ako(worm, fruit).

test(isa_fact, [nondet]) :- isa(maud, maggot).
test(isa_ako, [nondet]) :- isa(maud, grub).
test(isa_fail, [fail]) :- isa(maud, fruit).

test(rotten) :- rotten(test_subject2).
test(rotten_fail, [fail]) :- rotten(test_subject1).
% No mouldy apple in kb, better add one in [setup()] option
test(mouldy, [setup(assert(fact(a, mouldy, yes)))]) :- mouldy(a).
test(mouldy_fail, [fail]) :- mouldy(test_subject1).

test(gone_bad) :- gone_bad(test_subject2).
test(gone_bad_fail, [fail]) :- gone_bad(test_subject1).

test(infested) :- infested(test_subject3).
test(infested_fail, [fail]) :- infested(test_subject1).

test(edible) :- edible(test_subject1).
test(edible_bad, [fail]) :- edible(test_subject2).
test(edible_maggot, [fail]) :- edible(test_subject3).

:- end_tests(inference).

This is how we setup a suite of tests for plunit. Now let’s run them.

Running The Tests

THERE ARE two ways to run them, from the swipl prompt, or command line. When running from the swipl prompt, if you’re still adding tests, include make(all) in the options so when you query make. it also makes your tests. If you’ve done with that you can omit it: load_test_files([]).

bash:~/ $ swipl inference.pl
Welcome to SWI-Prolog <etc...>

?- load_test_files([make(all)]).
true.
?- run_tests.

From the command line you can call:

swipl -t "load_test_files([]), run_tests." -s inference.pl

This tell’s swipl to load the source file inference.pl and to run the tests as a top goal. I like to set an alias for this for easy testing, you can add the following to the end of your .bashrc, or set it locally, assuming you’re a fellow bash user.

alias plunit='swipl -t "load_test_files([]), run_tests." -s'

Now you can run the tests by simply typing:

plunit inference.pl

Conclusion

WE’VE GOT unit tests in their own file, working with separate knowledge base and reasoning. Plus, we can run them very easily. I’ve not gone into detail about how to write unit tests or how to use plunit. The docs are actually not too difficult to follow once you’ve seen an example and got a setup running. Hopefully, this has been helpful enough to get you started!