More Macros
Putting my test library to use, I tried to implement a simple password strength checker. I immediately realised that I needed a better way to organize my tests and test output. The current test output looks like this:
Test `it should return strong if it contains at least 1 lowercase alphabet, at least 1 uppercase alphabet, 1 special character, 1 digit and its length is at least 8` passed.
Test `it should return weak if it contains no lowercase alphabet even though it has at least 1 uppercase alphabet, 1 special character, 1 digit and its length is at least 8` passed.
Test `it should return weak if it contains no uppcase alphabet even though it has at least 1 lowercase alphabet, 1 special character, 1 digit and its length is at least 8` passed.
Test `it should return weak if it contains no special character even though it has at least 1 lowercase alphabet, 1 uppercase alphabet, 1 digit and its length is at least 8` passed.
Test `it should return weak if it contains no digit even though it has at least 1 lowercase alphabet, 1 uppercase alphabet, 1 special character and its length is at least 8` passed.
Test `it should return moderate if its length is less than 8 but at least 6 even though it has at least 1 lowercase alphabet, 1 uppercase alphabet, 1 special character, 1 digit` passed.
Test `it should return moderate if it contains at least 1 lowercase alphabet, 1 uppercase alphabet, 1 special character and its length is at least 6` passed.
Test `it should return weak if it contains no lowercase alphabet, even though it contains at least 1 uppercase alphabet, 1 special character and its length is at least 6` passed.
Test `it should return weak if it contains no uppcase alphabet even though it has at least 1 lowercase alphabet, 1 special character and its length is at least 6` passed.
Test `it should return weak if it contains no special character even though it has at least 1 lowercase alphabet, 1 uppercase alphabet and its length is at least 6` passed.
Test `it should return weak if its length is less than 6 even though it has at least 1 lowercase alphabet, 1 uppercase alphabet and 1 special character` passed.
11 out of 11 tests passed
Ideally, I want to be able to write tests with context
and test
, not unlike how other languages organize tests:
(context "When password length is at least 8"
(context "When ...."
(test "it should..." (...))))
And corresponding indented output like:
When password length is at least 8
When .....
Test `it should...` passed.
1 of 1 tests passed.
I ended up rewriting the entire library, applying additional macro techniques that I have learnt recently:
(let ((indent 0)
(outcomes '()))
(defun indent+ ()
(incf indent))
(defun indent- ()
(decf indent))
(defun add-outcome (outcome)
(push outcome outcomes))
(defun reset-outcomes ()
(setq outcomes '()))
(defun report-outcomes ()
(format t "~A out of ~A tests passed~%" (count t outcomes) (length outcomes)))
(defmacro test (name predicate)
(let ((result (gensym)))
`(let ((,result ,predicate))
(add-outcome ,result)
(format t "~&~ATest `~A` ~:[failed~;passed~].~%"
(make-string ,indent :initial-element #\space) ,name ,result))))
(defmacro context (name &rest body)
`(progn
(format t "~&~A~A~%" (make-string ,indent :initial-element #\space) ,name)
(indent+)
,@body
(indent-)
(if (zerop ,indent)
(progn (report-outcomes)
(reset-outcomes))))))
Admittedly a bit long, but it provides the support for nesting context
s and test
s. To make sure that previous work that went into writing tests using the previous library version don’t go to waste,
I have adapted run-tests
and make-test
to become macros that will recursively expand to use context
and test
primitives:
(defmacro run-tests (&rest body)
`(context "" ,@body))
(defmacro make-test (name predicate)
`(test ,name ,predicate))
Therefore, no change required for old tests!