如何在 Vim 中实践 Python TDD

-- xyb [DateTime(2004-09-22T03:59:57Z)] TableOfContents

如何在 Vim 中实践 Python TDD

作者:Xie Yanbo,版权:创作共用/cc 1.0

TDD,Test Driver Development,中文叫做测试驱动开发。这是最近几年比较受推崇的开发模式,也是 XP(敏捷开发方法) 中的重要组成部分。

关于 TDD 的书籍现在中文版的也有了,相关概念可以去看书或在网上查找。

在 Python 中,也提供了 xUnit 的 Python 实现:[http://pyunit.sourceforge.net/ PyUnit],在源代码中是一个叫做 unittest 的模块,它提供了对单元测试的支持。还有许多使用 Python 的项目,都利用 PyUnit 开发了自己更适用、更方便的测试框架,比如 Twisted 有 trail,Zope 有 ZopeTestCase,Plone 有 PloneTestCase。关于 PyUnit 的一些基本知识请参考 PyUnitTut

这个文档着重于讨论如何在 Vim 编辑器中使用 unittest 来实践 TDD。

首先要配置我们的 Vim,使之更适合 Python TDD。详细资料请参考 VimPython

我们开发的案例是一个产品库,用来保存不同产品、不同版本的源代码。我们知道,开源社区有大量优秀的项目供我们无偿使用(感谢那些开发者),那些使用广泛的项目大多更新迅速,这是开源的优点,但也给使用者带来一些更新上的麻烦。比如我开发的 Zope 系统,每天都有新版本的第三方代码出现,我在自己的程序中使用了很多,每次当我制造发布包都是一件麻烦的事情。所以我想到,应该有一个保存这些不同产品、不同版本源代码的产品库,可以在我需要时很快的找到它们。当然我们这里讨论的只是一个 TDD 方法的介绍,不可能把整个系统都写完,但如果有兴趣的朋友可以把这个程序继续开发下去,为大家造福。如果我有时间或空闲,说不定我也会继续它的开发的 :)

首先我们要确定需求。需求当然越详细越好,但我们自己的程序往往刚开始只有一个想法,TDD 对这个情况照样也很适应。我想从最基本的功能来说,这个产品库肯定要有添加产品的某个版本文件的功能,也要有从库里把这个产品、这个版本取出来的能力──这是最核心的功能了。

我们先来做入库的功能。我们开启 vim,建立一个新文件 test_filelib.py:

vim test_filelib.py

看到了吗,在 TDD 中,必须先写测试、再去编写它的实现。这个文件最开始只是一个空空的框架文件:

   1 #!/usr/bin/env python
   2 # -*- coding: GB2312 -*-
   3 
   4 from unittest import TestCase
   5 
   6 class simpleTest(TestCase):
   7     def setUp(self):
   8         pass
   9 
  10     def tearDown(self):
  11         pass
  12 
  13     def testExample(self):
  14         self.assertEqual(1, 1)
  15 
  16 if '__main__' == __name__:
  17     import unittest
  18     unittest.main()

添加我们的入库测试代码,把做为例子的 testExample 删掉,我想这个入库的函数应该是这样使用和测试的: {{ #!python

}}}

在还没有考虑到任何实现方法的情况下,我认为应该把产品名、版本号、要加入的文件的名字传给入库方法。不管它是否真的能这么做,反正我现在是这么想的。好,现在我们执行一下这个测试:在 Vim 的命令模式下键入 :make。如果你使用的是 Gvim,可以在工具栏上找到一个 make 的快捷按钮,点击它。在我的 Vim 里,出现了这样的提示:

:!python test_filelib.py  2>&1| tee /tmp/v874609/3
E
======================================================================
ERROR: testAddProduct (__main__.simpleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_filelib.py", line 20, in testAddProduct
    result = addProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
NameError: global name 'addProduct' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)
(6 / 11):  NameError: global name 'addProduct' is not defined

看来它在告诉我,我还没有定义这个 addProduct 这个方法呐!呵呵,不要担心,在 TDD 看来,这是正常的。TDD 的原则就是:测试先行!当我们准备好了测试,才会去编写它的编码。好了,我们现在知道 test_filelib.py 是语法无误的,它可以编译运行,那我们就开始编写代码的实现。先在测试文件中写下导入的命令:

from filelib import *

然后在 Vim 的命令模式键入命令:

:new filelib.py

这时 Vim 中出现了一个新的窗口,其中编辑的是我们的新文件 filelib.py,我们键入如下代码:

# -*- coding: GB2312 -*-

"""管理不同产品、不同版本的文件库
"""

def addProduct(ProductName, Version, Filename):
    return True

执行 :make 操作,这时 Vim 提示:

:!python ./alltests.py  2>&1| tee /tmp/v878704/2
python: can't open file './alltests.py'
(1 / 1): python: can't open file './alltests.py'

原来我们忘了加上 alltests.py,文件,好的,新增一个 alltests.py 文件,并把测试 test_filelib 写入它的测试列表中:

   1 #!/usr/bin/env python
   2 # -*- coding: GB2312 -*-
   3 
   4 import unittest
   5 import sys
   6 sys.path.append('./')
   7 sys.path.append('../')
   8 sys.path.append('./tests')
   9 
  10 modules_to_test = (
  11 'test_filelib',
  12 )
  13 
  14 def suite():
  15     alltests = unittest.TestSuite()
  16     for module in map(__import__, modules_to_test):
  17         alltests.addTest(unittest.findTestCases(module))
  18     return alltests
  19 
  20 if __name__ == '__main__':
  21     unittest.main(defaultTest='suite')

关闭 alltests.py,回到 filelib.py 中,重新执行 :make,这次的提示很完美:

:!python ./alltests.py  2>&1| tee /tmp/v878704/5
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
(1 / 5): .

测试报告告诉我们,我们的第一个测试已经通过!

也许有人要说了,这是在编程序吗?呵呵,TDD 的另一个原则:快速实现。快速实现允许你先不去考虑那么多,用一个最快的方法使测试通过,不管那个方法是多么可笑、多么简陋。但要记得,它是要与 TDD 的另一个原则一起使用,才能保证你的代码质量:重构。要记得随时随地审视代码,考虑它的更简洁、更合理的实现。现在我们就来重构一下这个函数。先得把它存到一个地方:

待续……