python -m unittest 没告诉你要跑哪个文件,它是怎么找到的?哪些会被悄悄漏掉?这篇造了一个「故意有干扰项」的测试目录——名字不对的文件、缺 __init__.py 的目录、导入就会报错的文件、不继承 TestCase 的类——然后让 unittest 真的去发现一遍。
tests/__init__.py tests/check_util.py tests/strings_test.py tests/test_broken.py tests/test_math.py tests/test_strings.py tests/nopkg/test_deep.py tests/pkg/__init__.py tests/pkg/test_inner.py
其中 tests/strings_test.py、tests/check_util.py 文件名不合规;tests/nopkg/ 没有 __init__.py;tests/test_broken.py 会在 import 时失败;test_math.py 里有不以 test 开头的方法和一个没有继承 TestCase 的类。
python -m unittest discover -s tests -t . -v
[import] tests/pkg/test_inner.py 被导入
[import] tests/test_math.py 被导入
[import] tests/test_strings.py 被导入
test_inner (tests.pkg.test_inner.TestInner.test_inner) ... ok
tests.test_broken (unittest.loader._FailedTest.tests.test_broken) ... ERROR
test_a_first (tests.test_math.TestAdd.test_a_first) ... ok
test_b_second (tests.test_math.TestAdd.test_b_second) ... ok
test_c_third (tests.test_math.TestAdd.test_c_third) ... ok
test_expected_fail (tests.test_strings.TestUpper.test_expected_fail) ... expected failure
test_skipped (tests.test_strings.TestUpper.test_skipped) ... skipped '演示跳过'
test_upper (tests.test_strings.TestUpper.test_upper) ... ok
======================================================================
ERROR: tests.test_broken (unittest.loader._FailedTest.tests.test_broken)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_broken
Traceback (most recent call last):
File "/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/unittest/loader.py", line 433, in _find_test_path
module = self._get_module_from_name(name)
File "/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/unittest/loader.py", line 374, in _get_module_from_name
__import__(name)
~~~~~~~~~~^^^^^^
File "<项目>/tests/test_broken.py", line 2, in <module>
import module_that_does_not_exist
ModuleNotFoundError: No module named 'module_that_does_not_exist'
----------------------------------------------------------------------
Ran 8 tests in 0.000s
FAILED (errors=1, skipped=1, expected failures=1)[import] 只有三行:test_inner、test_math、test_strings。strings_test.py、check_util.py 的导入行没有出现——它们根本没被导入;nopkg/test_deep.py 也没有——整个目录被跳过,而且没有任何提示。test_broken.py 变成了名叫 unittest.loader._FailedTest.tests.test_broken 的 ERROR 用例,其余照常运行。 discover 过程中 loadTestsFromModule 被调用的模块及其找到的用例数:
tests 0
tests.pkg 0
tests.pkg.test_inner 1
tests.test_math 3
tests.test_strings 3
总用例数 suite.countTestCases() = 8
全部用例 id:
tests.pkg.test_inner.TestInner.test_inner
tests.test_math.TestAdd.test_a_first
tests.test_math.TestAdd.test_b_second
tests.test_math.TestAdd.test_c_third
tests.test_strings.TestUpper.test_expected_fail
tests.test_strings.TestUpper.test_skipped
tests.test_strings.TestUpper.test_upper
unittest.loader._FailedTest.tests.test_broken自己写一个 TestLoader 子类记录 loadTestsFromModule 的调用,就能看到:每个被导入的模块都调用一次,包括 tests、tests.pkg 这两个包(0 个用例);test_math 3 个用例——只有 TestAdd 里以 test 开头的三个方法被收集,helper_check、check_things 和不继承 TestCase 的 NotATestCase.test_ignored 都没进来。也就是说 unittest 只认「TestCase 子类 + test 开头的方法」,不看类名叫不叫 Test。
[import] tests/test_math.py 被导入 test_a_first (tests.test_math.TestAdd.test_a_first) ... ok test_b_second (tests.test_math.TestAdd.test_b_second) ... ok test_c_third (tests.test_math.TestAdd.test_c_third) ... ok
源码里方法的写法顺序是 b、a、c,运行顺序却是 a、b、c:同一个类里的用例按方法名的字母序排序。所以不要依赖「写在前面的先执行」;需要固定顺序就在名字里编号,或者(更好)让用例彼此独立。
$ python -m unittest discover -s tests -t . -k upper -v tests.test_broken (unittest.loader._FailedTest.tests.test_broken) ... ERROR test_upper (tests.test_strings.TestUpper.test_upper) ... ok Ran 2 tests in 0.000s FAILED (errors=1) $ python -m unittest (不带参数, 等价于 discover) Ran 8 tests in 0.000s FAILED (errors=1, skipped=1, expected failures=1)
-k upper 只选出名字里含 upper 的用例,但导入失败的那个 ERROR 依然被报告(它没有被过滤),所以最后是 FAILED (errors=1)。不带任何参数的 python -m unittest 等价于 discover,从当前目录开始找,同样跑出 8 个。
unittest(discover 默认规则)load_tests 协议、setUpModule/setUpClass 的调用时机、与 pytest 收集规则的差异(见下一篇)、旧版本对命名空间包的支持