原章节名直译应该是:“建立单元测试框架”,感觉有些唬人,为了不至于霸气侧漏,就借用了下多数编程语言教程中多少都会提笔带过的“单元测试”了。
其实作为初级教程,是否有必要在单元测试上过多着墨,就要见仁见智了。个人看法是:理想很丰满,现实很骨感。呃。。。哪来那么多感慨,那就开始吧。(为什么要单元测试可以看
这里)
先举个测试的例子:
(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行代码而已!当然,这一切才刚开始..
(未完待续)
分享到:
相关推荐
《Practical Common Lisp-1st-2005》是一本专注于Common Lisp编程语言的实用书籍,作者Peter Seibel通过这本书向读者展示了如何使用Common Lisp来解决真实世界中的问题,强调程序员作为工程师和艺术家的双重身份,而...
common-lisp-the-language-second-edition.PDF
《Practical Common Lisp笔记》是一本深入探讨Common Lisp编程语言的实用教程。Common Lisp是一种功能强大的多范式编程语言,以其动态类型、宏系统和丰富的内置数据结构而闻名。这篇笔记详细记录了作者在学习过程中...
cad-lisp-3-表操作.LSP.lsp
《Common Lisp the Language, 2nd Edition》是Guy L. Steele所著的一本关于Common Lisp编程语言的权威指南,作为第二版,它不仅继承了前一版的经典,还对Lisp语言进行了深入的扩展和更新。这本书是Lisp爱好者的宝贵...
标题中的"emacs-lisp-intro-2.04.tar.gz"是一个典型的压缩文件名,它表明这个文件是一个关于Emacs Lisp的介绍性资料,并且版本号是2.04,格式为tar.gz。tar.gz是一种在Linux和Unix系统中常用的文件压缩格式,它先用...
标题:“实用Common.Lisp编程.pdf” 描述:“实用Common.Lisp编程.pdf,2011.10出版” 从这些信息中,我们可以提炼出几个关键的知识点: ### Common Lisp语言简介 Common Lisp是一种高级的、通用的、多范式的编程...
在压缩文件中,只有一个名为"practical_common_lisp.pdf"的文件,这是本书的PDF版本。由于原版PDF文件较大,经过7z压缩后,文件大小显著减小,便于下载和存储,但同时也能保持较好的图像和文字质量,满足读者的阅读...
Provides practical advice for the construction of Common Lisp programs. Shows examples of how Common Lisp is best used. Illustrates and compares features of the most popular Common Lisp systems on ...
AutoLisp源文件--标注高程.LSP
ANSI Common Lisp 中文翻译版.pdf 此资源是 ANSI Common Lisp 的中文翻译版,涵盖了 Common Lisp 语言的基础知识和高级主题。该资源包含了 17 章节,从基础的列表、特殊数据结构、控制流程、函数、输入与输出、符号...
这本《Practical Common Lisp》之所以号称Practical,正是因为这本书大量介绍Common Lisp在现实世界中的各种应用方式,算是第一本「入世传教」的Common Lisp著作。《Practical Common Lisp》是目前最畅销的Common ...
2. **单元测试框架**:书中介绍了一种用于编写测试用例的方法,并且展示了如何使用Common Lisp构建一个完整的单元测试框架。 3. **Web 开发**:尽管Common Lisp在Web开发领域不如Python或JavaScript那样流行,但它...
Practical Common Lisp 学习lisp的入门书籍
计算多个数字之和、计算多条线段长度之和、插入墙高标注、查询多段线顶点坐标并绘制、自动生成页码、绘制示坡线、插入排水箭头 https://blog.csdn.net/qq_24141055/article/details/121446354
Common-Lisp-Actors, 通用Lisp的actor系统 这是一个简单且易于使用的Actor系统,在。设置需要波尔多螺纹。http://common-lisp.net/project/bordeaux-threads/ 2. 加载 actors.lisp 并开始使用它。 如果你有 Quick
If you’re interested in Lisp as it relates to Python or Perl, and want to learn through doing rather than watching, Practical Common Lisp is an excellent entry point. — Chris McAvoy, Chicago Python ...
标题中的“Lisp-music-player.rar”表明这是一个基于Lisp语言开发的音乐播放器软件,其源代码或可执行文件被压缩在RAR格式的文件中。RAR是一种流行的压缩格式,通常用于存储和分发多个文件,它允许用户将多个文件...
2. **单元测试框架**:另一个重要的实用案例是构建单元测试框架的过程。作者通过这个例子向读者展示了如何利用Common Lisp编写高效且可靠的测试代码。 3. **其他实用案例**:除了上述两个案例外,《Practical Common...