You can get it on GitHub.
This library can be used for Unit Testing your PicoLisp code.
Explanation: Unit Testing framework for PicoLisp
This document provides a short walkthrough of the source code for the PicoLisp-Unit testing framework.
I won't cover concepts which were discussed in previous source code explanations. You can read them here:
This document is split into a few sections:
- Namespace leaky globals: Avoiding side-effects with Namespaces.
- Internal functions: System calls and printing data to the terminal.
- Public functions: Executing and asserting tests
Make sure you read the README to get an idea of what this library does.
Namespace leaky globals
Previously, I thought namespaces would protect my variables from modifying the
I was wrong.
Here's an example:
: (setq *Myvar "I am a var") : (symbols 'mytest 'pico) -> pico mytest: (prinl *Myvar) -> "I am a var" # so far so good mytest: (setq *Myvar "You are a var") mytest: (symbols 'pico) : (prinl *Myvar) -> "You are a var"
What? Yes that's right, the
*Myvar which was originally in the
'pico (default) namespace was modified from within the
'mytest namespace. This is normal, but not expected, and quite dangerous actually.
Let's try again:
: (setq *Myvar "I am a var") : (symbols 'mytest 'pico) -> pico mytest: (local *Myvar) mytest: (prinl *Myvar) -> NIL # that looks promising mytest: (setq *Myvar "You are a var") mytest: (symbols 'pico) : (prinl *Myvar) -> "I am a var"
This is much better, as we can now guarantee "global" functions and variables will not accidentally create side-effects by overwriting existing functions or variables.
This change has been applied everywhere now, and only public/exported functions can affect the global namespace.
Here we discuss system calls and printing data to the screen with specific alignment.
A cool unit-testing framework always displays colours. Just ask anyone from Node.js land.
To achieve this, we make use of an external system call, the *NIX
[de colour (Colour) (cond ((assoc (lowc Colour) *Colours) (call 'tput "setaf" (cdr @))) ((= (lowc Colour) "bold") (call 'tput "bold")) (T (call 'tput "sgr0")) ) NIL ]
It's quite simple. The first condition checks if the
Colour is part of the
*Colours list. If yes, use
tput setaf to set the terminal colour.
The second condition checks if the
bold. If yes, use
tput bold to set the text to bold.
The default catch-all (
T) resets the terminal back to normal.
I tend to stay away from external system calls as we're not always sure about the environment. In our case though, colour terminal is not such a big deal, and the
(colour) function will return
tput succedes or fails.
In some cases, using a combination of multiple printing functions can be helpful to achieve your designed results:
[de print-expected (Result) (prin (align 8 " ") "Expected: " (colour "green") ) (println Result) (colour) ]
This has 2 print statements, but it only prints one line. The first uses align to align the column to 8 spaces. This is really useful to help keep displayed text aligned in columns. The second prints the result and appends a newline at the end.
An alternative would have been:
[de print-expected (Result) (prin (align 8 " ") "Expected: " (colour "green") Result "^J" ) (colour) ]
^J character gets translated to a newline.
You'll notice we often call
(colour) without any arguments, to end-up in the catch-all mentioned earlier, which resets the terminal.
Public functions do all the work in this library. They execute a series of tests, and they assert results to see if your test should pass or fail.
I'll admit I was inspired mostly by Ruby's Minitest framework, which is quite huge compared to this one, but it pretty much does the same thing.
All good unit tests should be designed to run as units. O'Rly? Yeah. This means the order of the tests shouldn't matter at all. The units should not carry state, and this framework tests for that as well.
The magic happens in a simple
(randomize) function which takes the list of tests to execute, randomizes it, and then returns the list.
[de randomize (List) (if *My_tests_are_order_dependent List (by '((N) (rand 1 (size List))) sort List) ]
It first checks if the
*My_tests_are_order_dependent variable is
NIL (if it isn't, don't randomize).
To randomize, it uses by, not to be confused with
(bye) (that would be a major fail), and does stuff with it.
There's our anonymous function again, used as the 1st argument to
(by), which is cons'd to the
List (3rd argument), and then applied to the 2nd argument, which is the sort function.
The 1st argument (anonymous function) generates a random number between 1 and the size of the
It's crazy how that works. I'm not even sure how I came up with that.
(de execute @ (mapcar '((N) (prin (align 3 (inc '*Counter)) ") ") (eval N)) (randomize (rest)) ]
Once our list of tests is randomized, we run it through our favourite mapcar function which prints the test's number, stored in
*Counter, aligned to 3 columns, and then evaluates (runs) the test using the infamous eval.
(align 3) allows the test numbers to go from 1 to 999 without breaking the beautiful output. We can increase that when someone actually encounters that problem.
*Note: Technically, assertions don't catch errors, so if your assertion were to throw an unhandled error, then the entire test suite would fail and ugly things will happen. In fact, your terminals colours might not even get reset. That's a good thing. You should handle your errors.
PicoLisp natively supports assertions, and has a ton of predicates for testing and comparing values.
This library introduces simple wrappers around those predicates, which then call a
(failed) function with additional arguments.
[de assert-equal (Expected Result Message) (if (= Expected Result) (passed Message) (failed Expected Result Message) ]
This one is quite simple, all it does is check if
Expected is equal to
The other assertions are quite similar and seem to cover most test cases. I've considered adding opposite tests such as
refute, but I've rarely found a need for them as there are alternate approaches.