如何将Python 3单元测试分割到单独文件并用脚本控制运行?

1 投票
2 回答
2584 浏览
提问于 2025-04-18 18:13

我想把我的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()方法。

我遇到的问题是,我找不到一种文件组织方式,可以让我做到:

  1. 有多个模块,每个模块包含多个类和方法,且这些模块在不同的文件
  2. 使用一种包装脚本,能够对要运行的测试(模块/文件、类、方法)进行相同的控制。

我现在的问题是,我找不到一种方法,可以用run_tests.py脚本模拟python3 -m unittest的行为。例如,我希望能够做到:

  1. 运行当前目录下的所有测试 所以./run_tests.py -v应该和python3 -m unittest -v效果一样
  2. 运行一个模块(文件): ./run_tests.py -v TestSeqFunc相当于python3 -m unittest -v TestSeqFunc
  3. 运行一个类: ./run_tests.py -v TestSeqFunc.TestSequenceFunctions相当于python3 -m unittest -v TestSeqFunc.TestSequenceFunctions
  4. 运行类中的特定方法: ./run_tests.py -v TestSeqFunc.TestSequenceFunctions.test_sample相当于python3 -m unittest -v TestSeqFunc.TestSequenceFunctions.test_sample

请注意,我希望能够:

  1. 向单元测试传递参数,例如之前使用的详细标志;
  2. 允许运行特定的模块、类甚至方法。

目前,我在我的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 个回答

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)

注意事项:

  1. 我使用 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()

  2. 我必须添加 defaultTest=module_names,否则 main() 默认会执行当前文件中所有的测试类。因为在 run_all.py 中没有测试类,所以什么都不会执行。因此 defaultTest 必须指向一个包含所有模块名称的列表。

1

你可以通过 unittest.main 来传递命令行参数,使用的是 argv 参数

这个 argv 参数可以是一个选项列表,传递给程序时,第一个元素是程序的名字。 如果没有指定或者是 None,那么就会使用 sys.argv 的值。(这是我特别强调的)

所以你应该可以这样使用

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

而不需要任何改动,并且可以根据需要用命令行参数来调用你的脚本。

撰写回答