如何将Python 3单元测试分割到单独文件并用脚本控制运行?
我想把我的Python 3.4单元测试分成不同的模块,并且仍然能够从命令行控制哪些测试要运行或跳过,就像所有测试都在同一个文件里一样。现在我遇到了一些困难。
根据官方文档,可以使用命令行参数来选择要运行的测试。例如:
TestSeqFunc.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random
import unittest
class TestSequenceFunctions(unittest.TestCase):
def setUp(self):
self.seq = list(range(10))
def test_shuffle(self):
# make sure the shuffled sequence does not lose any elements
random.shuffle(self.seq)
self.seq.sort()
self.assertEqual(self.seq, list(range(10)))
# should raise an exception for an immutable sequence
self.assertRaises(TypeError, random.shuffle, (1,2,3))
def test_choice(self):
element = random.choice(self.seq)
self.assertTrue(element in self.seq)
def test_sample(self):
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
if __name__ == '__main__':
unittest.main()
可以通过以下方式控制:
./TestSeqFunc.py
来运行文件中的所有测试,
./TestSeqFunc.py TestSequenceFunctions
来运行TestSequenceFunctions类中定义的所有测试,最后:
./TestSeqFunc.py TestSequenceFunctions.test_sample
来运行特定的test_sample()
方法。
我遇到的问题是,我找不到一种文件组织方式,可以让我做到:
- 有多个模块,每个模块包含多个类和方法,且这些模块在不同的文件中
- 使用一种包装脚本,能够对要运行的测试(模块/文件、类、方法)进行相同的控制。
我现在的问题是,我找不到一种方法,可以用run_tests.py
脚本模拟python3 -m unittest
的行为。例如,我希望能够做到:
- 运行当前目录下的所有测试
所以
./run_tests.py -v
应该和python3 -m unittest -v
效果一样 - 运行一个模块(文件):
./run_tests.py -v TestSeqFunc
相当于python3 -m unittest -v TestSeqFunc
- 运行一个类:
./run_tests.py -v TestSeqFunc.TestSequenceFunctions
相当于python3 -m unittest -v TestSeqFunc.TestSequenceFunctions
- 运行类中的特定方法:
./run_tests.py -v TestSeqFunc.TestSequenceFunctions.test_sample
相当于python3 -m unittest -v TestSeqFunc.TestSequenceFunctions.test_sample
请注意,我希望能够:
- 向单元测试传递参数,例如之前使用的详细标志;
- 允许运行特定的模块、类甚至方法。
目前,我在我的run_all.py
脚本中使用了一个suite()
函数,它手动加载模块并使用addTest(unittest.makeSuite(obj))
将它们的类添加到一个测试套件中。然后,我的main()函数很简单:
if __name__ == '__main__':
unittest.main(defaultTest='suite')
但是使用这个我无法运行特定的测试。最后,我可能会在run_all.py
脚本中直接执行python3 -m unittest <sys.argv>
,但那样看起来不太优雅……
有什么建议吗?!
谢谢!
2 个回答
这是我最终的 run_all.py 文件:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import unittest
import glob
test_pattern = 'validate_*.py'
if __name__ == '__main__':
# Find all files matching pattern
module_files = sorted(glob.glob(test_pattern))
module_names = [os.path.splitext(os.path.basename(module_file))[0] for module_file in module_files]
# Iterate over the found files
print('Importing:')
for module in module_names:
print(' ', module)
exec('import %s' % module)
print('Done!')
print()
unittest.main(defaultTest=module_names)
注意事项:
我使用 exec() 来模拟 'import modulename'。问题是,使用 importlib(这里有个例子解释)虽然可以导入模块,但不会为模块的内容创建一个命名空间。当我输入
import os
时,会创建一个 "os" 的命名空间,这样我就可以访问os.path
。但是用 importlib 的话,我找不到创建这个命名空间的方法。拥有这样的命名空间是unittest
所必需的;否则会出现这样的错误:Traceback (most recent call last): File "./run_all.py", line 89, in <module> unittest.main(argv=sys.argv) File "~/usr/lib/python3.4/unittest/main.py", line 92, in __init__ self.parseArgs(argv) File "~/usr/lib/python3.4/unittest/main.py", line 139, in parseArgs self.createTests() File "~/usr/lib/python3.4/unittest/main.py", line 146, in createTests self.module) File "~/usr/lib/python3.4/unittest/loader.py", line 146, in loadTestsFromNames suites = [self.loadTestsFromName(name, module) for name in names] File "~/usr/lib/python3.4/unittest/loader.py", line 146, in <listcomp> suites = [self.loadTestsFromName(name, module) for name in names] File "~/usr/lib/python3.4/unittest/loader.py", line 114, in loadTestsFromName parent, obj = obj, getattr(obj, part) AttributeError: 'module' object has no attribute 'validate_module1'
所以我才使用
exec()
。我必须添加
defaultTest=module_names
,否则main()
默认会执行当前文件中所有的测试类。因为在run_all.py
中没有测试类,所以什么都不会执行。因此defaultTest
必须指向一个包含所有模块名称的列表。
你可以通过 unittest.main
来传递命令行参数,使用的是 argv
参数:
这个 argv 参数可以是一个选项列表,传递给程序时,第一个元素是程序的名字。 如果没有指定或者是 None,那么就会使用 sys.argv 的值。(这是我特别强调的)
所以你应该可以这样使用
if __name__ == '__main__':
unittest.main(defaultTest='suite')
而不需要任何改动,并且可以根据需要用命令行参数来调用你的脚本。