原章节名直译应该是:“建立单元测试框架”,感觉有些唬人,为了不至于霸气侧漏,就借用了下多数编程语言教程中多少都会提笔带过的“单元测试”了。
其实作为初级教程,是否有必要在单元测试上过多着墨,就要见仁见智了。个人看法是:理想很丰满,现实很骨感。呃。。。哪来那么多感慨,那就开始吧。(为什么要单元测试可以看[url=http://baike.baidu.com/view/106237.htm]这里[/url])
先举个测试的例子:
以上便是一个最简单的测试案例,收集、汇聚了传递不同参数值的情况下,函数的运行状况,不过也很容易看出,当结果为T时,自然全部符合预期,但如果为 NIL,天知道是哪一种预期出现了意外。于是乎,便有了下面的改造:
注意:“~:[FAIL~;pass~]”是一种特定格式,由第一个值的真伪来确定输出FAIL还是pass.
执行下:
是不是和预想中的结果有些接近了?不过似乎有点别扭的感觉,很是有些冗赘啊。还有就是,没有统一的结果认定(要知道这里只有三行,如果成百上千行那几屏都看不过来阿)。
既然发现了问题,解决之道也就不远了。重构一下吧!(不用担心,才几行代码啊,用不着腿肚打颤)。
先把最明显的重复语句“format”析出:
再来看看上面的改良版成什么样子了:
果然干净了不少,不过如果后半段简化一下:
似乎不错哦。
当当当。。。:
是不是很有些个成就感?什么?写那么多check也很累的?好吧:
这样总满意了吧。上面的宏最终会将代码展开成这个样子:
代码简单了,不过之前说过的第二个问题还木有解决,同志尚需努力啊。
其实关于如何统一结果,整合显示,问题也不是太过复杂,先对report-result简单改造下:
还需要一个专门用于收集结果的东东,类似于这个样子:
仔细想想该怎么实现:
然后修正下我们的check,用combine-results替代progn:
再来试试我们的测试案例:
是不是感到心满意足了?再好好想想,如果,咱要同时再测个乘法运算呢?
看起来还行啊,不过似乎有些方法名不显眼,量大容易花了眼。。
是不是感觉更靠谱了?好的,先深呼吸平复下心情。让我们从这儿倒推着往上看,有没有琢磨出些更深层次的东西?我们是要写单元测试,而凡是单元测试,除了主体之外,大部分都是相同或类似的东东。那么能不能对测试函数做一下更高层次的抽象?写个宏试试是否能个生成测试函数:
再写一次测试函数:
至此,似乎已经达到了既定目标--一个简单、通用的测试框架已经展现在我们眼前。是不是又有疑惑了?(为什么要用个又字),之前用来统一、整合测试结果用的函数test-arithmetic本身算不算测试函数呢?它能否也用那个宏来实现?这里就涉及到测试层级的问题。我个人的理解就是:test-+,test-*这些是第一层级的测试函数(最底层,也是目标),test-arithmetic则属于第二层测试函数,他为第一层服务,当然也可以有更高层级的,而这些层级主要取决于测试目标的量级。或许,用代码更容易表述这个观点:
怎么样,是不是直观多了。
好了,整理下完整的测试框架代码:
包含注释、空行,也只用了26行代码而已!当然,这一切才刚开始..
(未完待续)
其实作为初级教程,是否有必要在单元测试上过多着墨,就要见仁见智了。个人看法是:理想很丰满,现实很骨感。呃。。。哪来那么多感慨,那就开始吧。(为什么要单元测试可以看[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行代码而已!当然,这一切才刚开始..
(未完待续)