Unit testing and using assertion/1

A while back Jan noted that my use of assertion/1 in unit test should be avoided and instead the check done in the head of the test because it gives better error message, etc. (Not exact wording but should be close. If someone finds the post let me know or edit this post and add the link).

I never really understood that comment and in writing test cases would try and put the check in the head as well as using assertion/1 when possible looking for differences. After a few months found a test that hits the head on the nail for me at least in starting to understand the reasoning.


:- begin_tests(dl_append).

dl_append_test_case_generator(     Hole_1  ,Hole_1 ,    Hole_2  ,Hole_2 ,       Hole_3  ,Hole_3 ).
dl_append_test_case_generator(     Hole_1  ,Hole_1 ,[[]|Hole_2] ,Hole_2 ,[[]   |Hole_3] ,Hole_3 ).
dl_append_test_case_generator( [[]|Hole_1] ,Hole_1 ,    Hole_2  ,Hole_2 ,[[]   |Hole_3] ,Hole_3 ).
dl_append_test_case_generator( [[]|Hole_1] ,Hole_1 ,[[]|Hole_2] ,Hole_2 ,[[],[]|Hole_3] ,Hole_3 ).

test(01,[forall(dl_append_test_case_generator(Open_list_1,Hole_1,Open_list_2,Hole_2,Open_list_3,Hole_3))]) :-

test(02,[forall(dl_append_test_case_generator(Open_list_1,Hole_1,Open_list_2,Hole_2,Expected_open_list_3,Expected_hole_3))]) :-

    assertion( Open_list_3 =@= Expected_open_list_3 ),
    assertion( Hole_3      =@= Expected_hole_3 ).

:- end_tests(dl_append).

Run of test cases

?- run_tests(dl_append).
% PL-Unit: dl_append ........ done
% All 8 tests passed

Notice that test 01 does the check in the head of the test and that test 02 does the check using assertion/1.

The key difference is that with the check in the head (01) it is unifying the actual variables, while with the use of assertion/1 the actual variables are not used in the assertions and so =@= must be used.

Another case where using assertion/1 causes a problem but using unification in the test works better is when checking a generator for a subset of the results.

A generator is a predicate that generates values, typically for a set. If the predicate is written so that it can be run as the most general query, then none, some or all of the variables can be bound in the call. Now if one wants to check just one of the values generated that is not the first value then using assertion/1 will not work because the first time the generator is called it will generate a value which binds the variables. Now if the values bound are not the same as the test case values used with assertion, the test will fail. However if assertion/1 is not used then the predicate can backtrack and possibly find a solution.

The predicate generator_01/1 is a generator of the numbers 1 to 4.

File: generators.pl

:- module(generators,

:- load_test_files([]).

generator_01(N) :-

Example use of using generator_01/1.

?- generator_01(N).
N = 1 ;
N = 2 ;
N = 3 ;
N = 4.

?- generator_01(1).

?- generator_01(2).

?- generator_01(4).

?- generator_01(5).

File: generators.plt

:- begin_tests(generators).

:- use_module(generators).


test(bad_test,[true,nondet,forall(generator_test_case(_,success,Expected_n))]) :-

    assertion( N == Expected_n ).

test(good_test,[true,nondet,forall(generator_test_case(_,success,N))]) :-

:- end_tests(generators).

Examples of running test cases.

% Run bad test by itself.
?- run_tests(generators:bad_test).
% PL-Unit: generators:bad_test .
ERROR: c:/users/eric/documents/projects/swi-prolog/generators.plt:26:
        test bad_test (forall bindings = [2,2]): assertion failed
        Assertion: 1==2
A done
% 1 assertion failed
% 1 test failed
% 1 tests passed

% Run good test by itself.
?- run_tests(generators:good_test).
% PL-Unit: generators:good_test .. done
% All 2 tests passed

% A normal run of all tests.
?- run_tests.
% PL-Unit: generators .
ERROR: c:/users/eric/documents/projects/swi-prolog/generators.plt:26:
        test bad_test (forall bindings = [2,2]): assertion failed
        Assertion: 1==2
A.. done
% 1 assertion failed
% 1 test failed
% 3 tests passed
1 Like