Lisp ninja 6
It's not hard to get caught up on cranking out code when you are in the zone. All of the unit tests are passing one after another, and the implementation just flows out from your finger tips. There's no need to stop, step back and question the work because everything feels so right. Situations like this can really hinder your ability to unleash the power of your language. Especially if Lisp is your language of choice. After all, Lisp is the official programmable programming language.
This recently happened to me. I have been working on a side project (I'll talk about it in later post) that required me to implement string functions in Common Lisp that are similar, in spirit, to ones found in PHP. Since I'm a huge fan of TDD, I started with a failing test. However, before the failing test can be written, I need to confirm the actual behavior in PHP. During my the implementation cycle, I toggled back and forth between PHP and Lisp. This can get tedious over time, but I just kept chugging along.
This morning Kyle Burton showed me the light. He sent me a few pieces of code that remove the tediousness from my implementation cycle. I adopted the code to fit my environment and the testing framework. Without further adieu:
(defparameter *php-bin* "/opt/local/bin/php")
(when (not (probe-file *php-bin*))
(error
"Error: %s was not found, please update *php-bin* to point to your php."
*php-bin*))
(defun get-result-for-test-case (instr
&optional (width 75)
(lbreak #\Newline)
(cut nil))
(with-output-to-string (str)
(run-program *php-bin* (list
"-r"
(format nil "echo wordwrap(\"~a\",~a,\"~a\",~a);"
instr
width
lbreak
(if cut "true" "false")))
:output str)))
(get-result-for-test-case "aaron feng" 3)I was executing PHP code externally just to get the output, so I can use it in my unit tests. Why not ask PHP from Lisp!
The last line calls get-result-for-test-case function which calls the PHP wordwrap function directly and returns the result. Now this removed the need for me having to go outside my Lisp environment while I'm hacking. Why stop here? Why not have a macro that generates the test case using the result from PHP?
(defmacro create-test-case (test-name string &optional (width 75)
(lbreak #\Newline) (cut nil))
`(addtest ,test-name
(let ((input ,string)
(expected ,(get-result-for-test-case string width lbreak cut)))
(ensure-same (wordwrap input ,width ,lbreak ,cut) expected))))
(create-test-case empty-string " " 4)Voila, instant test case. the last line shows how the test case can be generated on the fly and run. If you are a real ninja, you need to take one more step further.
(defmacro create-test-cases (&rest cases)
`(prog2
(deftestsuite wordwrap-suite () ())
,@(mapcar #'(lambda (case) (macroexpand-1 `(create-test-case
,@case))) cases)))
(create-test-cases
(empty-string1 " ")
(empty-string2 " " 4)
(no-wrap "foo")
(wrap-one-word-at-1 "foo" 1))create-test-cases creates a testing suite to host all the test cases then it generates test cases based on input. This takes WAY LESS typing than hand writing each test case. I no longer have to switch between two environments (PHP and Lisp) because it's all automatically done for me! Thanks Kyle!

It's not just that it's less typing - you're less likely to make mistakes (reducing the LOC), you're more easily able to see the test case data itself (it's all lined up). You've created a mini-DSL for testing your code.
I was on the fence about whether to one-time generate the test cases (and thus not require an installation of php to run them), have the php output be created at compile time (macro expansion time) or leave it up till the time the test case is run. I guess there are a lot of ways this could have gone...
Why did you use prog2 in create-test-cases?
Why stop there?
With slight modification to
get-result-for-test-case, it can be generalized for any PHP function, not justwordwrap, which is especially easy when the Lisp function and the PHP function have the same name.Did you know that this little gadget by Xach uses Lisp to generate PHP?
Kyle,
Good question. I originally wanted to return the second form because I added deftestsuite to create-test-cases. Since there are no side effects, it doesn't actually matter.
Jonathan,
I also thought about it too. I'm not sure if the super generic version will out weight the ability to allow Lisp to do error checking for you? But this should be interesting.
Leslie,
No, sounds pretty interesting.