bigywater.tech
Python · 97

unittest discovery:测试框架如何找到 TestCase

python -m unittest 没告诉你要跑哪个文件,它是怎么找到的?哪些会被悄悄漏掉?这篇造了一个「故意有干扰项」的测试目录——名字不对的文件、缺 __init__.py 的目录、导入就会报错的文件、不继承 TestCase 的类——然后让 unittest 真的去发现一遍。

8 个用例
这棵树里被发现并运行的用例数(另有几个写好的用例因为文件名或目录问题没被发现)
无 __init__.py
子目录被整个跳过,且没有任何警告
_FailedTest
导入失败的测试文件变成一个 ERROR 用例,不会中断发现

做的事:一棵有干扰项的测试树

实测 · 本机执行输出
   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 的类。

验证一:discover 实际跑了什么

实测 · 本机执行输出
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)

验证二:用 TestLoader 看发现的过程

实测 · 本机执行输出
  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:同一个类里的用例按方法名的字母序排序。所以不要依赖「写在前面的先执行」;需要固定顺序就在名字里编号,或者(更好)让用例彼此独立。

验证四:-k 过滤与不带参数

实测 · 本机执行输出
$ 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 个。

分步演示:discover 逐项处理这棵目录树

每一项的结论都是上面真实运行的结果
unittest 如何发现测试
当前处理的文件

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 unittest(discover 默认规则)
    • 没有验证:load_tests 协议、setUpModule/setUpClass 的调用时机、与 pytest 收集规则的差异(见下一篇)、旧版本对命名空间包的支持
    京ICP备2026067502号-1京公网安备11010502063487号