星期六, 十一月 10, 2007

python unittest TestSuite 框架实践和几个问题

之前的 python Tree 实现中单元测试做的不好,在"Progmatic Unit Testing", 心得和自省中说过这一点。现在要重写单元测试。

为了保证所有测试的独立性和正交性,编写测试 setattr() 操作正确性的代码如下:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "09 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

"""Unittest for tree.Tree"""

from gettext import gettext as _

import os
import unittest
import inspect

import tree
from tree import Tree,TreeExc,TreeTypeExc,TreePathConvExc

# def _setAll(self):
# names = []
# for member in inspect.getmembers(self, inspect.ismethod):
# method_name = member[0]
# if method_name.startswith("_set"):
# names.append(method_name)
# names.remove("_setAll")
# for name in names:
# method = getattr(self, name)
# method()

# def testPreviousAll(self):
# # """The previous Tree nodes should not be affected by the latter operations"""
# self._setToggle = 0
# names = []
# for test_name in inspect.getmembers(self, inspect.ismethod):
# if test_name.startswith("test"):
# names.append(test_name)
# names.remove("testPreviousAll")
# # #1:
# suite = unittest.TestSuite(map(self.__class__, test_names))
# suite.run()
# # #2:
# # for name in names:
# # test_method = getattr(self, name)
# # test_method()

class TestTree(unittest.TestCase):
def setUp(self):
self._setToggle = 1
### self._setAll = _setAll
### self.testPreviousAll = testPreviousAll
### Why can't ??????

def tearDown(self):
self.root = None

def assertNodeSet(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node._Tree__node_value, value)

def assertNodeValue(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node(), value)

def assertNodeIndex(self, node, key, value):
self.failUnless(node._Tree__node_items.has_key(key))
self.failUnless(isinstance(node[key], Tree))
self.failUnlessEqual(node[key]._Tree__node_value, value)

def assertNotNodeIndex(self, node, key):
self.failIf(node._Tree__node_items.has_key(key))

def assertNodeIndexValue(self, node, key, value):
self.failUnless(node.has_key(key))
self.assertNodeValue(node[key], value)

def _setBaseCase(self):
self.root = Tree("root", data="root.data", extra="extra")

def testBaseCase(self):
# """The simplest assignment should create a Tree instance with several valid sub nodes"""
if self._setToggle:
self._setBaseCase()
self.assertNodeSet(self.root, "root")
self.assertNodeSet(self.root.data, "root.data")
self.assertNodeSet(self.root.extra, "extra")

def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
# """The previous Tree nodes should not be affected by the latter operations"""
self._setToggle = 0
# self.tearDown()
# self.setUp()
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
# suite = unittest.TestSuite(map(self.__class__, names))
# suite.run(suite)
# #2:
for name in names:
test_method = getattr(self, name)
test_method()

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()

def testNameReserved(self):
# """Assign to reserved names should be avoided"""
__used_names = object.__dict__.copy().keys() + [
'_Tree__path_stack', '_Tree__id_visited', '_Tree__used_names',
'_Tree__node_value', '_Tree__node_items',
'__path_stack', '__id_visited', '__used_names', '__node_value', '__node_items',
'__getattr__', '__setitem__', '__getitem__', '__call__',
'__add__', '__iadd__', '__cmp__',
'_one_node_set', '__traverse__', '__update__', '__copy__', '__search__'
]
for used_name in __used_names:
try:
setattr(self.root, used_name, 1)
self.fail(_("A reserved name '%s' should can not be reassignable" % used_name))
except TreeExc:
pass
except TypeError:
pass

def _setCreateNew(self):
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))

def _setAssignNonTreeToExisted(self):
self.root.nt_ex_simple = 1
self.root.nt_ex_complex = Tree(1, data="nt_ex_complex/data", extra="nt_ex_complex/extra")
# Create new first
self.root.nt_ex_simple = "one"
self.root.nt_ex_complex = "ONE"

def testAssignNonTreeToExisted(self):
# """If attribute is an existed Tree, and target is not a Tree, only the node value should be replaced"""
if self._setToggle:
self._setAssignNonTreeToExisted()
self.assertNodeSet(self.root.nt_ex_simple, "one")
self.assertNodeSet(self.root.nt_ex_complex, "ONE")
# If the node has childs, only value replacement:
self.assertNodeSet(self.root.nt_ex_complex.data, "nt_ex_complex/data")
self.assertNodeSet(self.root.nt_ex_complex.extra, "nt_ex_complex/extra")

def _setAssignTreeToExisted(self):
self.root.tr_ex_simple = 2
self.root.tr_ex_complex = Tree(2, data="tr_ex_complex/data")
self.root.tr_ex_simple_branch = '2'
self.root.tr_ex_complex_branch = Tree(2, data="tr_ex_complex_branch/data")
# Create first
self.root.tr_ex_simple = Tree("two")
self.root.tr_ex_complex = Tree("TWO")
self.root.tr_ex_simple_branch = Tree("_two", data="tr_ex_simple_branch/data")
self.root.tr_ex_complex_branch = Tree("_TWO", extra="tr_ex_complex_branch/extra")

def testAssignTreeToExisted(self):
# """If attribute is an existed Tree, and target is a Tree too, the node itself should be replaced"""
if self._setToggle:
self._setAssignTreeToExisted()
# (1) target Tree instance does not have attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple, "two")
# (2) target Tree instance does not have attributes, and original Tree have sub nodes:
self.assertNodeSet(self.root.tr_ex_complex, "TWO")
self.failIf(hasattr(self.root.tr_ex_complex, "data"))
# (3) target Tree instance has attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple_branch, "_two")
self.assertNodeSet(self.root.tr_ex_simple_branch.data, "tr_ex_simple_branch/data")
# (4) target Tree instance has attributes, and original Tree have sub nodes too:
self.assertNodeSet(self.root.tr_ex_complex_branch, "_TWO")
self.failIf(hasattr(self.root.tr_ex_complex_branch, "data"))
self.assertNodeSet(self.root.tr_ex_complex_branch.extra, "tr_ex_complex_branch/extra")

def _setReserveSomeAttrWhenReplaceNode(self):
self.root.replace_but_reserve_1 = ["Replace", "But", "Reserve", "Method", 1]
self.root.replace_but_reserve_1.br1 = 1
subtree = self.root.replace_but_reserve_1.br1
self.root.replace_but_reserve_1 = Tree("replaced_by_method_1", data="another_1")
self.root.replace_but_reserve_1.br1 = subtree

self.root.replace_but_reserve_2 = ["Replace", "But", "Reserve", "Method", 2]
self.root.replace_but_reserve_2.br2 = 2
self.root.replace_but_reserve_2 = Tree("replaced_by_method_2", br2=self.root.replace_but_reserve_2.br2, data="another_2")

def testReserveSomeAttrWhenReplaceNode(self):
if self._setToggle:
self._setReserveSomeAttrWhenReplaceNode()
self.assertNodeSet(self.root.replace_but_reserve_1, "replaced_by_method_1")
self.assertNodeSet(self.root.replace_but_reserve_1.br1, 1)
self.assertNodeSet(self.root.replace_but_reserve_1.data, "another_1")
self.assertNodeSet(self.root.replace_but_reserve_2, "replaced_by_method_2")
self.assertNodeSet(self.root.replace_but_reserve_2.br2, 2)
self.assertNodeSet(self.root.replace_but_reserve_2.data, "another_2")

......
这里首先从 TestCase 继承一个 TestTree 基类,并自定义一些 assert 测试方法来做一些基本的测试。这样测试 setattr() 的类 TestSetAttr 可以从 TestTree 继承并直接调用这些 assert 来完成更复杂的测试,这样也就提高了复用性。

每一个 test* 方法基本上和一个 _set* 方法对应,并只进行很简单很专门的测试,这样就可以将方法名定义得更具有可读性,而且方便其他测试方法来调用,test* 和 _set* 分开也是为了这个目的。

比如,上面在 TestTree 中定义的 testPreviousAll() 方法,就是为了测试之前所有的操作是否互相影响,比如对一个节点的子节点进行操作之后,它本身的值、它的兄弟节点和其他子节点都不应该受到影响,它会利用 inspect 模块提供的功能寻找所有自己这个测试类中的 test* 方法(排除自身),并逐一调用,以确保这一点。因为 TestSetAttr 是从 TestTree 继承的,所以它也会有这个 testPreviousAll() 方法。

要逐一调用这些方法方法,有两种想法,其一是利用 getattr() 得到这个方法的实例,并直接调用;另一种思路是利用 TestSuite 的构造方法得到一个 test suite,并调用 suite.run() 直接运行。

除了利用 testPreviousAll() 方法来做这件事外,我一开始的另一个思路是在后面调用 TextTestRunner().run(suite) 的时候调用对 TestSetAttr 前后调用两次,而不用使用 testPreviousAll() 方法。

无论使用那种方法,前后两次调用 test* 方法都需要分别设定 _setToggle 标志,保证第二次调用的时候,test* 方法不会去调用 _set* 重复设定各个节点,这样才能得到正确的测试结果。

但是这几种思路都有问题。逐一来讨论。

首先,更深入的了解一下 unittest 的框架。TestCase 为一个基本单元,由若干 TestCase 组成一个 TestSuite,是可以运行的单元,TestSuite 也可以包含 TestSuite。但必须通过调用 TestRunner 的实例来运行 TestSuite 的实例。例如:
suite = unittest.TestSuite()
suite.addTest(...)
...
unittest.TextTestRunner(verbosity=2).run(suite)
在 Python 的官方手册中,提到一个 test fixture,这个 fixture 事实上并没有相应的 class,只不过是一种概念,即可以将若干包含 TestSuite 的 TestSuite 看作一个 fixture,因为其环境比较复杂了。如何对这样一个 fixture 设置 setUp() 和 tearDown() 环境这里不讨论。

为了运行所有这些测试,最简单的办法是直接调用 unittest.main() 函数:
if __name__ == "__main__":
unittest.main()
它会自动去寻找当前模块里所有的 tests 组成 TestSuite 并运行之。因此这段代码的等效代码可以看作是:
if __name__ == "__main__":
import __main__
suite = unittest.TestLoader().loadTestsFromModule(__main__)
unittest.TextTestRunner(verbosity=2).run(suite)
所以问题的关键是如何得到这些 TestSuite 并将 tests 加入其中。

除了调用 unittest.main() 以外,显式地创建 test suite 有很多方法,除了上面的 TestLoader 之外,还可以逐个加入:
suite = unittest.TestSuite()
suite.addTest(TestSetAttr("testNameReserved"))
suite.addTest(TestSetAttr("testCreateNew"))
...
unittest.TextTestRunner(verbosity=2).run(suite)
通过阅读 unittest 的源代码可以知道,addTest() 的参数即为一个 test,也就是一个 TestCase 的 instance,并且这个 instance 的 testMothod() 方法指向传递给构造函数的方法名所表示的方法。

TestSuite.addTests() 的参数是 tuple list of tests。

另一种方法是:
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
因为 TestSetAttr 是 callable,所以 map(function, seq1, [seq2, ...]) 会将它作为一个函数调用,并将 test_names 依次作为其调用时的参数。这样就会依次构建所有的 tests instance。

回到这个测试的具体案例上来。如果采用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 的做法,有一个问题就是 tests 不会按照你定义他们的时候的上下顺序来运行(而且即使按照定义的顺序来运行也没有用,下面谈到),但是在定义测试的时候,如最开始所描述的,必须要考虑到对 Previous 条件进行测试的需要。如果我不做 testPreviousAll(),那么这样运行没有什么问题,但问题就在于做 testPreviousAll() 是有必要的。

一开始,使用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 这种方法,在 TestTree 这个基类中定义的方法如下:
def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
self._setToggle = 0
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
suite = unittest.TestSuite(map(self.__class__, names))
suite.run(suite)
# unittest.TextTestRunner(verbosity=2).run(suite)
# #2:
# for name in names:
# test_method = getattr(self, name)
# test_method()
然后运行:
 python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

----------------------------------------------------------------------
Ran 19 tests in 0.462s

FAILED (errors=3)
显然,你不能向 suite.run() 传递 suite 作为参数,你只能向 suite.run() 传递 TestResult 的 instance 作为参数,或者将 suite 作为参数传递给 TestRunner 的 instance,亦即在上面的定义中使用的另外一种方法:unittest.TextTestRunner(verbosity=2).run(suite)。但这样运行会导致你的输出很不规整,你可能会得到类似这样的结果:
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.157s

OK
ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.124s

OK
ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.006s

OK
ok

----------------------------------------------------------------------
Ran 19 tests in 0.759s
这样显然是不利于测试的组织和代码的排错的。所以改用在上面的定义中的 #2 所指明的办法。

在上面 testPreviousAll() 的定义中调用了 self._setAll(),这看上去会降低正交性,但是不再其中定义却不行,你会得到这样的错误:

# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 360, in testDeepIndexedNodes
self.assertNodeSet(self.root.deep, "deep_indexed_prefix")
AttributeError: Tree instance has no attribute 'deep'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 168, in testAssignNonTreeToExisted
self.assertNodeSet(self.root.nt_ex_simple, "one")
AttributeError: Tree instance has no attribute 'nt_ex_simple'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 84, in testBaseCase
self.assertNodeSet(self.root, "root")
AttributeError: 'TestTree' object has no attribute 'root'

----------------------------------------------------------------------
Ran 19 tests in 0.304s

FAILED (errors=3)
因为通过构造 TestSuite 的方法可以看出,每一次传递给它的构造函数的都是一个全新的 TestCase instance,并且每一个这样的 instance 中在运行的时候只有其中的一个 test* 方法被运行,就是 testMothod 所指向的那个方法。所以如果不先调用这个方法,那么任何节点都不会设置。

如果使用方法 #1,则似乎不会出现这个问题,所有测试都 OK 通过,但其实是不对的。因为在检查 PreviousAll 的时候,我必须避免对要检查的节点重复设置,所以在 test* 方法中使用了 _setToggle 标志来实现这一点。但因为使用 TestSuite() 创建了新的 tests,而这些 tests 的 _setToggle 必然是重新设置为 1 的,所以所有节点都会被重复设置,因此测出的结果是不正确的。如果将 _setToggle 定义为 class 的 static 变量呢?似乎可行,但显然会降低正交性,并且如前所述,测试的结果输出很不规范。

这也就是前面提到,即使按照顺序来调用这些 tests 也没有用的原因。如果要按照顺序来调用,可以这样:
fixture = unittest.TestSuite()
test_names = ["testBaseCase"]
suite = unittest.TestSuite(map(TestTree, test_names))
fixture.addTest(suite)
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
fixture.addTest(suite)
test_names = [
"testNodeWithoutKey",
"testNodeWithKeys",
"testInexistentNode",
"testOnlyValueReplacement",
"testNodeReplacement",
"testIndexedParentReplacement",
"testUnhashable",
"testDeepIndexedNodes",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestKeyIndex, test_names))
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=3).run(fixture)
当然这样意义不大,而且显然增加了重复性。

即使是在 testPreviousAll() 中调用了 _setAll() 也仍然是有问题的,因为
for member in inspect.getmembers(self, inspect.ismethod)
这段代码也不会按照你定义的顺序去 inspect member methods,所以 _setBaseCase() 可能会在中间运行再次被运行(setUp() 里面运行过第一次,否则没有根节点,我这里实际上就在中间重新运行的),因此导致 root 节点被重新设置,结果是已经设置的其他节点被冲掉了,因此后面必然会抛出 AttributeError 异常。当然结果是不确定的,可能会出现这样的问题,也可能会巧合的按照定义顺序进行。

显然第一种思路是行不通了。那么第二种思路呢?
fixture = unittest.TestSuite()
suite = unittest.TestLoader().loadTestsFromTestCase(TestTree)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
TestSetAttr._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
TestKeyIndex._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=2).run(fixture)
这时候,不定义 testPrevious() 和 _setAll() 方法,并且 _setToggle 定义为类的 static 变量。结果当然仍然是不行,可以推想,因为所有的 tests 都是全新的 TestCase instance,相互之间没有关联,因此第二次产生的 suite 并不会使用第一次产生的 suite 的结果。在将 TestSetAttr._setToggle 设置为 0 后,调用 unittest.TestLoader().loadTestsFromTestCase(TestSetAttr) 产生的 tests,其 test* 将不再设置 _set* 方法,因此所有的节点都不会被设置,必然会抛出 AttributeError。实际运行的结果也是如此。

那么如何解决这个正交性测试独立性的问题呢?

因为同时必须要考虑到顺序的问题,但同时因为各个测试方法之间都是互相独立的,所以我接下来的一个思路就是在每一个 test* 方法中调用定义的前一个 test* 和 _set*。代码大概是这样(此时不再需要定义 testPreviousAll() 和 _setAll()):
class TestTree(unittest.TestCase):
...
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()
...
def _setCreateNew(self):
self._setBaseCase()
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))
self._testPrevious("testBaseCase")
这样在运行 testCreateNew() 的时候,会接着运行 testBaseCase() 以确保前面设置的节点没有受到 CreateNew 操作的影响,并且因为关闭了 _setToggle,不会导致重复设置。

但如果把这个问题扩展一下呢?就是说,如果我按照任意的顺序去调用 _set* 方法,那么能否始终保证后面的操作不会对前面的已有结果造成破坏呢?

另外还有一个问题。在:
python assemble methods at runtime?

python unzip
中,曾提到运行时装配方法的技巧。但在这里似乎行不通,因为这时候这个 testPreviousAll() 方法好像根本就没有被检测到(使用 loadTestsFromModules(__main__)):
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 16 tests in 0.280s

OK
看一下 unittest 的源代码,其调用关系是这样的:
loadTestsFromModules()
\--> loadTestsFromTestCase() # TestSetAttr
\--> getTestCaseNames(self, testCaseClass)

class TestLoader:
......
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, TestSuite):
raise TypeError("Test cases should not be derived from TestSuite. Maybe you meant to derive from TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
return self.suiteClass(map(testCaseClass, testCaseNames))

def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)

def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))
testFnNames = filter(isTestMethod, dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in self.getTestCaseNames(baseclass):
if testFnName not in testFnNames: # handle overridden methods
testFnNames.append(testFnName)
if self.sortTestMethodsUsing:
testFnNames.sort(self.sortTestMethodsUsing)
return testFnNames
filter 是 builtin 函数,显然在使用 dir(testCaseClass) 的时候是不会包含 testPreviousAll() 的,因为 testPreviousAll()只会在 TestCase instance 中存在。

所以应该不是在 setUp() 方法里面设置,而是应该定义 TestTree.testPreviousAll = testPreviousAll。

包含这些问题的代码在 caxes/trunk/test/test_tree.bk 中:
__package__ = "caxes"
__revision__ = [257:259]

没有评论: