==========================
== Zhuo Hong Wei's Blog ==
==========================
Any and everything

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 contexts and tests. 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!