The below setup of using mock data fails. module_under_test fails to compile as there’s no “person” predicate in sight.
module_under_test.pl:
:- module(foo, [count_jims]).
% defines some predicates for working with the data, it pulls data using the predicates
% does not import data_module, because we want the test to define its own data
count_jims(N) :-
findall(P, person(jim, P), Ps),
length(Ps, N).
module_under_test.plt:
% tester for module_under_test
:- begin_tests(foo).
:- use_module(foo).
% defines its own controlled version of the data
person(jim, a).
% and runs tests of the behavior on this data.
test(find_jims) :-
...
:- end_tests(foo).
data_module.pl:
% defines some predicates that return inert data
% to be used in production, not in testing
person(jim, jones).
If there’s a solution that involves importing data_module into module_under_test.pl, I’ll take it, though I should say that seems like unnecessary coupling: it’s the main program that should pull in the right data, not module_under_test. The latter is meant to hold generic functions that work with any data setup.
The typical Prolog solution would be to define count_jims/1 as a meta predicate. Now you can import the meta predicates and the data into some module and call the meta predicates. E.g.
This will also nicely cooperate with the test because every :- begin_tests ... :- end_tests creates a module.
Normally, module-sensitive arguments of a meta-predicate are predicates. As this example shows, you can also cheat and use the : qualifier to make the target module available as qualification to any other argument.
The approach you presented seems quite intrusive, it seems like the entire public library surface of our program needs to be modified to accommodate the tests.
I wonder if there are solutions that can be deployed in the .plt file only. For instance, we could retract the real predicate and assert our own? It seems like the use of modules makes that hard.
We could also not use modules. I realize that the private predicates now leak into each other, but that’s a price we could pay for testability. At least predicates would be written as they normally are, without the module qualifications leaking everywhere.
Yes. As is, your analysis predicate and the data are connected. count_jims/1 does its work in the module where it is defined, foo. So, to test count_jims/1 with other data you’ll have to modify person/2 in the module foo or get count_jims/1 to operate on data in another module. The previously mentioned wrap_predicate/4 allows you to modify the definition in foo. Turning count_jims/1 into a meta predicate allows for disconnecting the definitions from the data it operates on. Another options is to put all these “compute” predicates into a file and use include/1 to include them into a place (module) where the data resides. That achieves more or less the same as using meta predicates, but does not require you to change the code. The price is that with including the code you get a duplicate definition of the predicates in multiple places, i.e., it uses more memory. The are not precise duplicates as they differ in the module to which the calls to the data predicates are resolved.
All these options seems acceptable to me. All except the wrapping approach are more or less portable across most Prolog implementations.
Maybe my resistance to creating an extension point using meta_predicate is overstated. We do make API surface concessions for the benefit of testing in other languages just as well, so why not make a concession here.
It could be useful to have an extension point allowing us to plug an entire module, though. Like having the test tell Prolog “override this module with my module”. Could the meta predicate support that and be a natural way to do it?
The easiest way is to have a second module with the same name and change the search path to load either one. I.e, you use
:- use_module(data(mydata)).
and using file_search_path/2 you define to which location data resolves. Now for testing, you make it resolve somewhere else. The price is that you cannot have it both ways at the same time, so the test must run in a separate Prolog process.
A runtime swap of modules is also possible. Needs a bit of reflection. You find the predicates imported into your foo module and use abolish/1 to remove them. Next you load your new data module into foo using
use_module(foo:my_test_data).
where we assume my_test_data.pl and the original data module export the same predicates. And, of course, you can also do the reflection and dynamically wrap the predicates of your data module with the ones from your test data. Most of this is non-portable though. The first approach is probably the closest to being portable, though Prolog systems find files in different ways. Using abolish/1 to remove an imported predicate is not portable and neither is predicate wrapping.
Another, somewhat portable. trick is to use term_expansion/2 in the test scenario to modify what is actually being defined. So, you can do
before loading foo.pl to make it load its data from elsewhere.
In addition to the primitives given, there are also import/1, export/1, add_import_module/3 and friends that allow all sorts of dynamically managing modules.
Depending on your requirements I assume one of these options will be acceptable.
Minimum imposition on the code itself. No imposition at all if I use abolish/1 instead of retractall/1. I assume retractall/1 is portable, so using dynamic/1 to allow it seems like a minimum of trouble.
Each test can assert different predicate setups to test different behaviors of the predicates under test.
No extra files containing test versions of predicates, which if you have two dozen components, can become an annoyance to maintain.
This solution uses different names from the original example. What used to be “foo” is now “people_tools”. That used to be “data” is now “people”. The former depends on the latter.
Hope this helps someone looking to set up their tests.
people.pl:
:- module(people, [person/2]).
% Not needed if we're okay with the tests using abolish/1
% instead of retractall/1.
:- dynamic(person/2).
person(jim, jones).
person(jim, souza).
person(john, jones).
person(jim, albertson).