2.1. 迭代1:测试框架的建立

2.1.1. 假想任务目标
2.1.2. 建立测试用例
2.1.3. 编写模组,使测试用例通过
2.1.4. 完善测试用例
2.1.5. 用例管理和 nosetests

首先搭建单元测试框架,并完成一个最小的功能集合。

2.1.1. 假想任务目标

首先为我们的模型起个名字:svnauthz

Subversion路径授权中,用户对象(用户/别名/组)显然是最重要的基本单位, 每一条授权策略都包含一个用户对象。那么我们第一个迭代就实现用户对象: User 类,Alias 类, Group 类。

假设 svnauthzUser, Alias, Group 类已经完成, 我们期望他们实现的功能是什么呢?于是在纸上写下假想任务目标(模拟python交互式命令行):

>>> from svnauthz import User, Group, Alias
>>> user1=User('Tom')
>>> user2=User("Jerry")
>>> print user1
Tom # 显示 user1 内容(字符串化)

>>> alias1=Alias('admin')
>>> alias1.user = user1
>>> print alias1
admin = Tom # 显示 alias1 内容(字符串化)

>>> group1 = Group('team1')
>>> group2 = Group('team2')
>>> group1.append(group2, user2, alias1, user1)
>>> print group1
team1 = &admin, @team2, Jerry, Tom # group1 的成员列表要进行排序
>>> group2.append(group1, user1)
Exception: ... # 抛出异常! group1 引起了组间的循环引用
>>> group2.append(group1, user1, autodrop=True)
>>> print group2
team2 = Tom # 使用 autodrop 参数,自动抛弃冲突的组成员,而不引发异常。(即容错性)

2.1.2. 建立测试用例

将假想的任务目标翻译为测试用例。建立单元测试文件 test_svnauthz.py 如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest
from svnauthz import *

class TestStage1(unittest.TestCase):

    def testUser(self):
        user1 = User('Tom')
        self.assert_(str(user1) == 'Tom')
    
    def testAlias(self):
        user1 = User('Tom')
        alias1=Alias('admin')
        alias1.user = user1
        self.assert_(str(alias1) == 'admin = Tom', str(alias1))
    
    def testGroup(self):
        user1 = User('Tom')
        user2 = User('Jerry')
        alias1=Alias('admin')
        alias1.user = user1
        group1 = Group('team1')
        group2 = Group('team2')
        group1.append(group2, user2, alias1, user1)
        self.assert_(str(group1) == 'team1 = &admin, @team2, Jerry, Tom')
        self.assertRaises(Exception, group2.append, group1, user1)
        group2.append(group1, user1, autodrop=True)
        self.assert_(str(group2) == 'team2 = Tom')

if __name__ == '__main__': unittest.main()

执行测试用例:

$ python test_svnauthz.py
Traceback (most recent call last):
  File "test_svnauthz.py", line 8, in <module>
    from svnauthz import *
ImportError: No module named svnauthz

测试失败!不要紧,因为我们还没有写代码呢。

2.1.3. 编写模组,使测试用例通过

之前执行测试用例失败,报告:找不到 svnauthz 模组。因为模组还没有创建,当然找不到了。 于是创建一个空的模组文件 svnauthz.py

$ touch svnauthz.py

执行测试用例:

$ python test_svnauthz.py
EEE
======================================================================
ERROR: testAlias (__main__.TestStage1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_svnauthz.py", line 17, in testAlias
user1 = User('Tom')
NameError: global name 'User' is not defined
...

太棒了,我们前进了一步,因为失败的原因已经不同了。错误报告说: User类未定义。于是我们写一些代码, 让测试用例通过。

svnauthz.py 第一个版本的代码如下:

1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 """Subversion authz config file management.
5 
6 Basic classes used for Subversion authz management.
7 """
8 
9 class User(object):
10 
11     def __init__(self, name):
12         name = name.strip()
13 
14         if not name:
15             raise Exception, 'Username is not provided'
16 
17         self.__name = name
18 
19     def __str__(self):
20         return self.__name

再次执行测试用例:

$ python test_svnauthz.py -v
testAlias (__main__.TestStage1) ... ERROR
testGroup (__main__.TestStage1) ... ERROR
testUser (__main__.TestStage1) ... ok

======================================================================
ERROR: testAlias (__main__.TestStage1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_svnauthz.py", line 18, in testAlias
alias1=Alias('admin')
NameError: global name 'Alias' is not defined
...

好的,我们已经有一个测试用例(testUser)通过了! 其他的测试用例呢?先把他们注释掉,以便提前感受一下完全通过测试的滋味。

注意:我所说的注释掉不是删除代码,也不是把每一行变为注释, 而是非常简单的将暂不考虑的测试用例改名。

  • def testAlias(self) 改为 def _testAlias(self)

  • def testGroup(self) 改为 def _testGroup(self)

[注意]

注:只要不是以 test 开头都好。

再次执行测试用例,太棒了完全通过!

$ python test_svnauthz.py -v
testUser (__main__.TestStage1) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

2.1.4. 完善测试用例

检查代码覆盖度,在 Python 下有 coverage 包可用。 用 easy_install 安装之后, 就可以使用 coverage 命令了。

$ coverage -x test_svnauthz.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
$ ls .coverage
.coverage
$ coverage -r -m svnauthz.py
Name       Stmts   Exec  Cover   Missing
----------------------------------------
svnauthz       8      7    87%   15

哦,看来我们离完美还是差了一点。从 coverage 的输出中可以看出,我们的测试用例并没有对 svnauthz.py 的代码测试完全:第15行没有测试到。也就是用空的用户名创建 User 对象,应该抛出异常。

我们在 testUser 用例的最后补充一条断言:

def testUser(self):
    user1 = User('Tom')
    self.assert_(str(user1) == 'Tom')
    self.assertRaises(Exception, User, " ")

再次检查一下测试用例对代码的覆盖度。哇,100% 通过!

$ coverage -x test_svnauthz.py
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
$ coverage -r -m svnauthz.py
Name       Stmts   Exec  Cover   Missing
----------------------------------------
svnauthz       8      8   100%

2.1.5. 用例管理和 nosetests

目前来讲,代码和测试用例共存于同一个目录。我们重构一下,将模组代码放在 src 目录,将测试用例放在 tests 目录。

执行测试用例:

$ python tests/test_svnauthz.py
Traceback (most recent call last):
  File "tests/test_svnauthz.py", line 8, in <module>
    from svnauthz import *
ImportError: No module named svnauthz

test_svnauthz.py 文件头增加如下语句, 设置 Python 模组查询路径:

import sys
sys.path.insert(0,'src')

测试用例又可以成功执行了。

目录 tests 下如果有多个测试用例文件, 难道要一个一个去调用么?或者用 unittest.TestSuite 去组织测试用例?其实不用这么麻烦,nosetests 可以自动发现目录下的测试用例,并执行。

鼻子测试(nosetests)是一个主动发现测试用例的 unittest 扩展。可以用 easy_install 来安装:

$ easy_install nose
$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.008s

OK

代码覆盖度测试

$ nosetests --with-coverage --cover-package=svnauthz
.
Name       Stmts   Exec  Cover   Missing
----------------------------------------
svnauthz       8      8   100%
----------------------------------------------------------------------
Ran 1 test in 0.030s

OK