Size: 6906
Comment: 如何在 Vim 中实践 Python TDD,未完待续……
|
Size: 15352
Comment:
|
Deletions are marked like this. | Additions are marked like this. |
Line 83: | Line 83: |
#!python | |
Line 93: | Line 94: |
#!python | |
Line 109: | Line 111: |
原来我们忘了加上 alltests.py,文件,好的,新增一个 alltests.py 文件,并把测试 test_filelib 写入它的测试列表中: | 原来我们忘了加上 alltests.py 文件,好的,新增一个 alltests.py 文件: |
Line 113: | Line 115: |
# -*- coding: GB2312 -*- | |
Line 117: | Line 118: |
sys.path.append('./') sys.path.append('../') sys.path.append('./tests') modules_to_test = ( 'test_filelib', ) |
import os sys.path.append(os.curdir) sys.path.append(os.pardir) sys.path.append(os.path.join(os.curdir, 'tests')) tests = os.listdir(os.curdir) tests = [n[:-3] for n in tests if n.startswith('test') and n.endswith('.py')] teststests = os.path.join(os.curdir, 'tests') if os.path.isdir(teststests): teststests = os.listdir(teststests) teststests = [n[:-3] for n in teststests if n.startswith('test') \ and n.endswith('.py')] modules_to_test = tests + teststests else: modules_to_test = tests |
Line 148: | Line 159: |
也许有人要说了,这是在编程序吗?呵呵,TDD 的另一个原则:快速实现。快速实现允许你先不去考虑那么多,用一个最快的方法使测试通过,不管那个方法是多么可笑、多么简陋。但要记得,它是要与 TDD 的另一个原则一起使用,才能保证你的代码质量:重构。要记得随时随地审视代码,考虑它的更简洁、更合理的实现。现在我们就来重构一下这个函数。先得把它存到一个地方: 待续…… |
也许有人要说了,这是在编程序吗?呵呵,TDD 的另一个原则:快速实现。快速实现允许你先不去考虑那么多,用一个最快的方法使测试通过,不管那个方法是多么可笑、多么简陋。但要记得,它是要与 TDD 的另一个原则一起使用,才能保证你的代码质量:重构。要记得随时随地审视代码,考虑它的更简洁、更合理的实现。现在我们就来审视一下这个函数。其实它是一个“伪实现”,可它还是骗过了我们的测试用例,这说明测试写的还不太严格。为了让测试更准确,得修改一下它。现在我想,应该检查是不是真的有指定的产品文件被保存起来了: {{{ #!python def testAddProduct(self): result = existsProduct('myproduct', '0.1', 'myproduct-0.1.tgz') self.assertEqual(result, False) result = addProduct('myproduct', '0.1', 'myproduct-0.1.tgz') self.assertEqual(result, True) result = existsProduct('myproduct', '0.1', 'myproduct-0.1.tgz') self.assertEqual(result, True) }}} 哦,原来应该有这么一个专门检查产品版本的东西,早先我怎么没想起来呢?!呵呵,不要苛求自己,在一小会儿时间里把一个项目里所有的函数都提前想到,这是任何人都办不到的事情。现在在 TDD 的帮助下,我们把隐藏的函数找出来了。运行 :make 测试,我们看到了预期中的错误,接下来要实现 existsProduct,并完善 addProduct,让它真的把文件存起来(existsProduct 在 testAddProducts 中被充分测试,我们就用不着专门给它写一个测试用例了)。 {{{ #!python import os import shutil def existsProduct(ProductName, Version, Filename): return os.path.exists(os.path.join(ProductName, Version, Filename)) def addProduct(ProductName, Version, Filename): pathname = os.path.join(ProductName, Version) if not os.path.exists(pathname): os.makedirs(pathname) return shutil.copyfile(Filename, os.path.join(pathname, Filename)) }}} 代码看起来很不错,我们来运行一下测试。可惜,出错了,原来是目前根本就没有这么一个文件“myproduct-0.1.tgz”。那么我们要准备一个这样的测试用的文件吗?经过思考,我想应该把 addProduct 的参数改变一下,增加一个 filehandle 参数,文件的数据流就从这个参数中传入。这可真是一个不错的主意,这样我们需要的数据就可以从任何一个类 file 的对象中读入了,比如内存中的 StringIO,或者是一个 urlopen 打开的网络连接。同时我们要准备的测试文件也解决了,用一个 StringIO 模拟就行了。这又是一个测试中想到的好主意。为什么我们会想到这么多以前没有想到的好主意呢?我看,这是因为我们在编写测试用例时,是做为这些函数、接口的使用者在工作。做为直接的实践者,这些接口使用是否方便,我们自然是心知肚明了。实际上,对于其它开发者来说,这些测试用例正是模块更新最及时、最易查询的使用手册!重新编码之后,测试代码变成这样: {{{ #!python from unittest import TestCase from StringIO import StringIO from filelib import * class simpleTest(TestCase): def setUp(self): self.pname = 'myproduct' self.pver = '0.1' self.pfilename = 'myproduct-0.1.tgz' self.pcontent = 'testfile\nmyproduct\n' self.pfd = StringIO(self.pcontent) def tearDown(self): pass def testAddProduct(self): result = existsProduct(self.pname, self.pver, self.pfilename, self.pfd) self.assertEqual(result, False) result = addProduct(self.pname, self.pver, self.pfilename, self.pfd) self.assertEqual(result, True) result = existsProduct(self.pname, self.pver, self.pfilename, self.pfd) self.assertEqual(result, True) }}} 这里,我把多次重复出现的产品名称、文件名之类放到变量中,并设置了模拟文件设备 self.pfd。这些准备工作都在 setUp 中完成,它会在每个测试用例之前被执行,构造全新的测试环境。这里用来模拟的 self.pfd 也被称为 Mock Object 或者 Stub(桩模块)。 再次运行测试,修改实现代码: {{{ #!python def addProduct(ProductName, Version, Filename, FileHandle): pathname = os.path.join(ProductName, Version) if not os.path.exists(pathname): os.makedirs(pathname) newfd = open(os.path.join(pathname, Filename), 'w') return newfd.write(FileHandle.read()) }}} 这次,测试报告告诉我们:AssertionError: None != True。原来是 file.write 根本不返回任何值的。改一下代码: {{{ #!python def addProduct(ProductName, Version, Filename, FileHandle): pathname = os.path.join(ProductName, Version) if not os.path.exists(pathname): os.makedirs(pathname) newfd = open(os.path.join(pathname, Filename), 'w') newfd.write(FileHandle.read()) return True }}} 再测。这次的提示是:AssertionError: True != False。怎么回事?我们还没有添加这个产品,它就已经存在了?!看看目录中的文件,原来我们上次测试中生成的文件还留在文件系统中。需要抛弃真实文件系统,使用自己模拟的吗?那代价太高了,还是用最快的方法吧,每次把上次遗留的测试目录删除。其实 Python 有 tempfile 模块,可以用它来创建我们自己的临时目录,我们应该好好利用一下。不过,现在的实现代码中,所有文件都保存在源代码所在的目录中,没有一个好的机制更改文件库的“根目录”,看来这也要改一下了。经过考虑,决定把 filelib 组织成一个类,在初始化时指定它的存储根目录。看,代码是不是更像那么回事了。测试用例: {{{ #!python from unittest import TestCase from StringIO import StringIO import tempfile from shutil import rmtree from filelib import * class simpleTest(TestCase): def setUp(self): self.pname = 'myproduct' self.pver = '0.1' self.pfilename = 'myproduct-0.1.tgz' self.pcontent = 'testfile\nmyproduct\n' self.pfd = StringIO(self.pcontent) self.root = tempfile.mkdtemp() self.filelib = ProductLib(self.root) def tearDown(self): rmtree(self.root) def testAddProduct(self): result = self.filelib.existsProduct( self.pname, self.pver, self.pfilename) self.assertEqual(result, False) result = self.filelib.addProduct( self.pname, self.pver, self.pfilename, self.pfd) self.assertEqual(result, True) result = self.filelib.existsProduct( self.pname, self.pver, self.pfilename) self.assertEqual(result, True) }}} 每次测试用例执行完毕,tearDown 都会把临时目录删除。运行测试,出错,但不是语法的问题。然后考虑实现: {{{ #!python class ProductLib: def __init__(self, rootpath): self.root = rootpath def existsProduct(self, ProductName, Version, Filename): return os.path.exists(os.path.join( self.root, ProductName, Version, Filename)) def addProduct(self, ProductName, Version, Filename, FileHandle): pathname = os.path.join(self.root, ProductName, Version) if not os.path.exists(pathname): os.makedirs(pathname) newfd = open(os.path.join(pathname, Filename), 'w') newfd.write(FileHandle.read()) return True }}} 好,测试,再次通过了!我们又多运行了几遍,还是没有任何问题 :D 现在,显然测试代码和实现代码都有很多要继续修改的地方,比如: * 测试新增的产品文件的内容就是我们提供给它的内容 * 重构 ProductLib,消除重复,把计算目录全名的代码放到一个独立函数中 * 显然我们开始随便起的 filelib.py 的名字不合时宜,应该把模块改名成 productlib 了 我们把它记在纸片上,留到明天一个一个处理 :) 总结,显然 TDD 有以下优点: * 可以让我们专著于小范围内的编码,有代码隔离的作用,减少编程时思考的复杂度 * 提前进入使用者的角度思考,使编出的接口稳定可靠 * 使代码随时保持在可运行、可发布状态! |
如何在 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
- def testAddProduct(self):
- result = addProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
self.assertEqual(result <> None, True)
- result = addProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
}}}
在还没有考虑到任何实现方法的情况下,我认为应该把产品名、版本号、要加入的文件的名字传给入库方法。不管它是否真的能这么做,反正我现在是这么想的。好,现在我们执行一下这个测试:在 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 是语法无误的,它可以编译运行,那我们就开始编写代码的实现。先在测试文件中写下导入的命令:
1 from filelib import *
然后在 Vim 的命令模式键入命令:
:new filelib.py
这时 Vim 中出现了一个新的窗口,其中编辑的是我们的新文件 filelib.py,我们键入如下代码:
执行 :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 文件:
1 #!/usr/bin/env python
2
3 import unittest
4 import sys
5 import os
6
7 sys.path.append(os.curdir)
8 sys.path.append(os.pardir)
9 sys.path.append(os.path.join(os.curdir, 'tests'))
10
11 tests = os.listdir(os.curdir)
12 tests = [n[:-3] for n in tests if n.startswith('test') and n.endswith('.py')]
13
14 teststests = os.path.join(os.curdir, 'tests')
15 if os.path.isdir(teststests):
16 teststests = os.listdir(teststests)
17 teststests = [n[:-3] for n in teststests if n.startswith('test') \
18 and n.endswith('.py')]
19 modules_to_test = tests + teststests
20 else:
21 modules_to_test = tests
22
23 def suite():
24 alltests = unittest.TestSuite()
25 for module in map(__import__, modules_to_test):
26 alltests.addTest(unittest.findTestCases(module))
27 return alltests
28
29 if __name__ == '__main__':
30 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 的另一个原则一起使用,才能保证你的代码质量:重构。要记得随时随地审视代码,考虑它的更简洁、更合理的实现。现在我们就来审视一下这个函数。其实它是一个“伪实现”,可它还是骗过了我们的测试用例,这说明测试写的还不太严格。为了让测试更准确,得修改一下它。现在我想,应该检查是不是真的有指定的产品文件被保存起来了:
1 def testAddProduct(self):
2 result = existsProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
3 self.assertEqual(result, False)
4 result = addProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
5 self.assertEqual(result, True)
6 result = existsProduct('myproduct', '0.1', 'myproduct-0.1.tgz')
7 self.assertEqual(result, True)
哦,原来应该有这么一个专门检查产品版本的东西,早先我怎么没想起来呢?!呵呵,不要苛求自己,在一小会儿时间里把一个项目里所有的函数都提前想到,这是任何人都办不到的事情。现在在 TDD 的帮助下,我们把隐藏的函数找出来了。运行 :make 测试,我们看到了预期中的错误,接下来要实现 existsProduct,并完善 addProduct,让它真的把文件存起来(existsProduct 在 testAddProducts 中被充分测试,我们就用不着专门给它写一个测试用例了)。
1 import os
2 import shutil
3
4 def existsProduct(ProductName, Version, Filename):
5 return os.path.exists(os.path.join(ProductName, Version, Filename))
6
7 def addProduct(ProductName, Version, Filename):
8 pathname = os.path.join(ProductName, Version)
9 if not os.path.exists(pathname):
10 os.makedirs(pathname)
11 return shutil.copyfile(Filename, os.path.join(pathname, Filename))
代码看起来很不错,我们来运行一下测试。可惜,出错了,原来是目前根本就没有这么一个文件“myproduct-0.1.tgz”。那么我们要准备一个这样的测试用的文件吗?经过思考,我想应该把 addProduct 的参数改变一下,增加一个 filehandle 参数,文件的数据流就从这个参数中传入。这可真是一个不错的主意,这样我们需要的数据就可以从任何一个类 file 的对象中读入了,比如内存中的 StringIO,或者是一个 urlopen 打开的网络连接。同时我们要准备的测试文件也解决了,用一个 StringIO 模拟就行了。这又是一个测试中想到的好主意。为什么我们会想到这么多以前没有想到的好主意呢?我看,这是因为我们在编写测试用例时,是做为这些函数、接口的使用者在工作。做为直接的实践者,这些接口使用是否方便,我们自然是心知肚明了。实际上,对于其它开发者来说,这些测试用例正是模块更新最及时、最易查询的使用手册!重新编码之后,测试代码变成这样:
1 from unittest import TestCase
2 from StringIO import StringIO
3 from filelib import *
4
5 class simpleTest(TestCase):
6 def setUp(self):
7 self.pname = 'myproduct'
8 self.pver = '0.1'
9 self.pfilename = 'myproduct-0.1.tgz'
10 self.pcontent = 'testfile\nmyproduct\n'
11 self.pfd = StringIO(self.pcontent)
12
13 def tearDown(self):
14 pass
15
16 def testAddProduct(self):
17 result = existsProduct(self.pname, self.pver, self.pfilename, self.pfd)
18 self.assertEqual(result, False)
19 result = addProduct(self.pname, self.pver, self.pfilename, self.pfd)
20 self.assertEqual(result, True)
21 result = existsProduct(self.pname, self.pver, self.pfilename, self.pfd)
22 self.assertEqual(result, True)
这里,我把多次重复出现的产品名称、文件名之类放到变量中,并设置了模拟文件设备 self.pfd。这些准备工作都在 setUp 中完成,它会在每个测试用例之前被执行,构造全新的测试环境。这里用来模拟的 self.pfd 也被称为 Mock Object 或者 Stub(桩模块)。
再次运行测试,修改实现代码:
这次,测试报告告诉我们:AssertionError: None != True。原来是 file.write 根本不返回任何值的。改一下代码:
再测。这次的提示是:AssertionError: True != False。怎么回事?我们还没有添加这个产品,它就已经存在了?!看看目录中的文件,原来我们上次测试中生成的文件还留在文件系统中。需要抛弃真实文件系统,使用自己模拟的吗?那代价太高了,还是用最快的方法吧,每次把上次遗留的测试目录删除。其实 Python 有 tempfile 模块,可以用它来创建我们自己的临时目录,我们应该好好利用一下。不过,现在的实现代码中,所有文件都保存在源代码所在的目录中,没有一个好的机制更改文件库的“根目录”,看来这也要改一下了。经过考虑,决定把 filelib 组织成一个类,在初始化时指定它的存储根目录。看,代码是不是更像那么回事了。测试用例:
1 from unittest import TestCase
2 from StringIO import StringIO
3 import tempfile
4 from shutil import rmtree
5 from filelib import *
6
7 class simpleTest(TestCase):
8 def setUp(self):
9 self.pname = 'myproduct'
10 self.pver = '0.1'
11 self.pfilename = 'myproduct-0.1.tgz'
12 self.pcontent = 'testfile\nmyproduct\n'
13 self.pfd = StringIO(self.pcontent)
14 self.root = tempfile.mkdtemp()
15 self.filelib = ProductLib(self.root)
16
17 def tearDown(self):
18 rmtree(self.root)
19
20 def testAddProduct(self):
21 result = self.filelib.existsProduct(
22 self.pname, self.pver, self.pfilename)
23 self.assertEqual(result, False)
24 result = self.filelib.addProduct(
25 self.pname, self.pver, self.pfilename, self.pfd)
26 self.assertEqual(result, True)
27 result = self.filelib.existsProduct(
28 self.pname, self.pver, self.pfilename)
29 self.assertEqual(result, True)
每次测试用例执行完毕,tearDown 都会把临时目录删除。运行测试,出错,但不是语法的问题。然后考虑实现:
1 class ProductLib:
2 def __init__(self, rootpath):
3 self.root = rootpath
4
5 def existsProduct(self, ProductName, Version, Filename):
6 return os.path.exists(os.path.join(
7 self.root, ProductName, Version, Filename))
8
9 def addProduct(self, ProductName, Version, Filename, FileHandle):
10 pathname = os.path.join(self.root, ProductName, Version)
11 if not os.path.exists(pathname):
12 os.makedirs(pathname)
13 newfd = open(os.path.join(pathname, Filename), 'w')
14 newfd.write(FileHandle.read())
15 return True
好,测试,再次通过了!我们又多运行了几遍,还是没有任何问题
现在,显然测试代码和实现代码都有很多要继续修改的地方,比如:
- 测试新增的产品文件的内容就是我们提供给它的内容
重构 ProductLib,消除重复,把计算目录全名的代码放到一个独立函数中
- 显然我们开始随便起的 filelib.py 的名字不合时宜,应该把模块改名成 productlib 了
我们把它记在纸片上,留到明天一个一个处理
总结,显然 TDD 有以下优点:
- 可以让我们专著于小范围内的编码,有代码隔离的作用,减少编程时思考的复杂度
- 提前进入使用者的角度思考,使编出的接口稳定可靠
- 使代码随时保持在可运行、可发布状态!