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.
Previously, I thought namespaces would protect my variables from modifying the 'pico
namespace.
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.
PicoLisp provides a nifty local function for fixing that. I admit it's a bit of a pain and quite ugly, but it does the job. You can consider it similar to JavaScript's var
expression.
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 tput
command.
[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 Colour
is 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 NIL
whether tput
succedes or fails.
Printing data to the screen is simple in PicoLisp, until you realize there are at least 5 known functions to do that: prin, prinl, print, println, and printsp. There's probably more.
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) ]
The ^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 List
.
It's crazy how that works. I'm not even sure how I came up with that.
[de execute @
(mapcar eval (randomize (rest) ]
Once our list of tests is randomized, we run it through our favourite mapcar function which evaluates (runs) the test using the infamous eval.
*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 (passed)
or (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 Result
.
The (passed)
and (failed)
functions will return T
or NIL
, respectively, so these can be used within your own code as well, not only in the context of unit tests.
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.
That's pretty much all I have to explain about the Unit Testing framework for PicoLisp. I'm very open to providing more details about functionality I've skipped, so just file an issue and I'll do my best.
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Copyright (c) 2015 Alexander Williams, Unscramble [email protected]