lisp初体验-Practical Common Lisp笔记-11.单元测试

原章节名直译应该是:“建立单元测试框架”,感觉有些唬人,为了不至于霸气侧漏,就借用了下多数编程语言教程中多少都会提笔带过的“单元测试”了。

其实作为初级教程,是否有必要在单元测试上过多着墨,就要见仁见智了。个人看法是:理想很丰满,现实很骨感。呃。。。哪来那么多感慨,那就开始吧。(为什么要单元测试可以看[url=http://baike.baidu.com/view/106237.htm]这里[/url])

先举个测试的例子:

(defun test-+ ()
(and
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))

CL-USER> (test-+)
T

以上便是一个最简单的测试案例,收集、汇聚了传递不同参数值的情况下,函数的运行状况,不过也很容易看出,当结果为T时,自然全部符合预期,但如果为 NIL,天知道是哪一种预期出现了意外。于是乎,便有了下面的改造:

(defun test-+ ()
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2) 3) '(= (+ 1 2) 3))
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

注意:“~:[FAIL~;pass~]”是一种特定格式,由第一个值的真伪来确定输出FAIL还是pass.
执行下:

CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
NIL

是不是和预想中的结果有些接近了?不过似乎有点别扭的感觉,很是有些冗赘啊。还有就是,没有统一的结果认定(要知道这里只有三行,如果成百上千行那几屏都看不过来阿)。

既然发现了问题,解决之道也就不远了。重构一下吧!(不用担心,才几行代码啊,用不着腿肚打颤)。
先把最明显的重复语句“format”析出:

(defun report-result (result form)
(format t "~:[FAIL~;pass~] ... ~a~%" result form))

再来看看上面的改良版成什么样子了:

(defun test-+ ()
(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))

果然干净了不少,不过如果后半段简化一下:

(check (= (+ 1 2) 3))

似乎不错哦。

(defmacro check (form)
`(report-result ,form ',form))

当当当。。。:

(defun test-+ ()
(check (= (+ 1 2) 3))
(check (= (+ 1 2 3) 6))
(check (= (+ -1 -3) -4)))

是不是很有些个成就感?什么?写那么多check也很累的?好吧:

(defmacro check (&body forms)
`(progn
,@(loop for f in forms collect `(report-result ,f ',f))))


(defun test-+ ()
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))

这样总满意了吧。上面的宏最终会将代码展开成这个样子:

(defun test-+ ()
(progn
(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4))))


代码简单了,不过之前说过的第二个问题还木有解决,同志尚需努力啊。
其实关于如何统一结果,整合显示,问题也不是太过复杂,先对report-result简单改造下:

(defun report-result (result form)
(format t "~:[FAIL~;pass~] ... ~a~%" result form)
result)

还需要一个专门用于收集结果的东东,类似于这个样子:

(combine-results
(foo)
(bar)
(baz))

(let ((result t))
(unless (foo) (setf result nil))
(unless (bar) (setf result nil))
(unless (baz) (setf result nil))
result)

仔细想想该怎么实现:

(defmacro combine-results (&body forms)
(with-gensyms (result)
`(let ((,result t))
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
,result)))
//这里的with-gensyms是不是很眼熟阿,呵呵

然后修正下我们的check,用combine-results替代progn:

(defmacro check (&body forms)
`(combine-results
,@(loop for f in forms collect `(report-result ,f ',f))))

再来试试我们的测试案例:

CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
T

CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
FAIL ... (= (+ -1 -3) -5)
NIL

是不是感到心满意足了?再好好想想,如果,咱要同时再测个乘法运算呢?

(defun test-* ()
(check
(= (* 2 2) 4)
(= (* 3 5) 15)))

(defun test-arithmetic ()
(combine-results
(test-+)
(test-*)))

CL-USER> (test-arithmetic)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
pass ... (= (* 2 2) 4)
pass ... (= (* 3 5) 15)
T

看起来还行啊,不过似乎有些方法名不显眼,量大容易花了眼。。

(defvar *test-name* nil)

(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)

(defun test-+ ()
(let ((*test-name* 'test-+))
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4))))

(defun test-* ()
(let ((*test-name* 'test-*))
(check
(= (* 2 2) 4)
(= (* 3 5) 15))))

CL-USER> (test-arithmetic)
pass ... TEST-+: (= (+ 1 2) 3)
pass ... TEST-+: (= (+ 1 2 3) 6)
pass ... TEST-+: (= (+ -1 -3) -4)
pass ... TEST-*: (= (* 2 2) 4)
pass ... TEST-*: (= (* 3 5) 15)
T

是不是感觉更靠谱了?好的,先深呼吸平复下心情。让我们从这儿倒推着往上看,有没有琢磨出些更深层次的东西?我们是要写单元测试,而凡是单元测试,除了主体之外,大部分都是相同或类似的东东。那么能不能对测试函数做一下更高层次的抽象?写个宏试试是否能个生成测试函数:

(defmacro deftest (name parameters &body body)
`(defun ,name ,parameters
(let ((*test-name* ',name))
,@body)))

再写一次测试函数:

(deftest test-+ ()
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))

至此,似乎已经达到了既定目标--一个简单、通用的测试框架已经展现在我们眼前。是不是又有疑惑了?(为什么要用个又字),之前用来统一、整合测试结果用的函数test-arithmetic本身算不算测试函数呢?它能否也用那个宏来实现?这里就涉及到测试层级的问题。我个人的理解就是:test-+,test-*这些是第一层级的测试函数(最底层,也是目标),test-arithmetic则属于第二层测试函数,他为第一层服务,当然也可以有更高层级的,而这些层级主要取决于测试目标的量级。或许,用代码更容易表述这个观点:

(let ((*test-name* (append *test-name* (list ',name))))
//注意了,变量做了些改动

(deftest test-arithmetic ()
(combine-results
(test-+)
(test-*)))

CL-USER> (test-arithmetic)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)
pass ... (TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)
pass ... (TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)
T

(deftest test-math ()
(test-arithmetic))

CL-USER> (test-math)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)
T

怎么样,是不是直观多了。
好了,整理下完整的测试框架代码:

(defvar *test-name* nil)

(defmacro deftest (name parameters &body body)
"Define a test function. Within a test function we can call
other test functions or use 'check' to run individual test
cases."
`(defun ,name ,parameters
(let ((*test-name* (append *test-name* (list ',name))))
,@body)))

(defmacro check (&body forms)
"Run each expression in 'forms' as a test case."
`(combine-results
,@(loop for f in forms collect `(report-result ,f ',f))))

(defmacro combine-results (&body forms)
"Combine the results (as booleans) of evaluating 'forms' in order."
(with-gensyms (result)
`(let ((,result t))
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
,result)))

(defun report-result (result form)
"Report the results of a single test case. Called by 'check'."
(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
result)

包含注释、空行,也只用了26行代码而已!当然,这一切才刚开始..

(未完待续)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值