如何解决unittest中同名模块批量测试失败的问题?
仓库结构
原代码仓库结构:
. └── Programs ├── program1 │ └── Generic_named.py └── program2 └── Generic_named.py
测试结构:
. ├── Programs │ ├── program1 │ │ └── Generic_named.py │ └── program2 │ └── Generic_named.py └── Tests ├── mock │ ├── 1 │ │ └── custom_module.py │ └── 2 │ └── custom_module.py ├── temp ├── test1.py └── test2.py
temp文件夹用于临时存放程序及mock依赖,各文件内容如下:
Programs/program1/Generic_named.py
import custom_module def main(): return custom_module.out()
Programs/program2/Generic_named.py
import custom_module def main(): return custom_module.out("Goodbye, World!")
Tests/mock/1/custom_module.py
def out():return "Hello, World!"
Tests/mock/2/custom_module.py
def out(x):return x
测试脚本
Tests/test1.py
import unittest import os import sys import shutil if os.path.exists('Tests/temp/1'): shutil.rmtree('Tests/temp/1') shutil.copytree('Tests/mock/1', 'Tests/temp/1/') shutil.copyfile('Programs/program1/Generic_named.py', 'Tests/temp/1/Generic_named.py') sys.path.append('Tests/temp/1') import Generic_named sys.path.remove('Tests/temp/1') class Test(unittest.TestCase): def test_case1(self): self.assertEqual(Generic_named.main(), "Hello, World!") if __name__ == '__main__': unittest.main()
Tests/test2.py
import unittest import os import sys import shutil if os.path.exists('Tests/temp/2'): shutil.rmtree('Tests/temp/2') shutil.copytree('Tests/mock/2', 'Tests/temp/2') shutil.copyfile('Programs/program2/Generic_named.py', 'Tests/temp/2/Generic_named.py') sys.path.append('Tests/temp/2') import Generic_named sys.path.remove('Tests/temp/2') class Test(unittest.TestCase): def test_case1(self): self.assertEqual(Generic_named.main(), "Goodbye, World!") if __name__ == '__main__': unittest.main()
问题现象
单独运行test1.py和test2.py均通过,但用python3 -m unittest discover -p test*.py -s Tests/批量运行时,test2失败:
.F ====================================================================== FAIL: test_case1 (test2.Test) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/s/Documents/Coding practice/2024/Test Mess/1/Tests/test2.py", line 18, in test_case1 self.assertEqual(Generic_named.main(), "Goodbye, World!") AssertionError: 'Hello, World!' != 'Goodbye, World!' - Hello, World! + Goodbye, World! ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1)
修改test2.py中临时文件名为Generic_named1.py后,批量运行报错:
.E ====================================================================== ERROR: test_case1 (test2.Test) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/s/Documents/Coding practice/2024/Test Mess/2/Tests/test2.py", line 18, in test_case1 self.assertEqual(Generic_named1.main(), "Goodbye, World!") File "/home/s/Documents/Coding practice/2024/Test Mess/2/Tests/temp/2/Generic_named1.py", line 4, in main return custom_module.out("Goodbye, World!") TypeError: out() takes 0 positional arguments but 1 was given ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (errors=1)
核心原因:Python会缓存已导入的模块,即使修改sys.path,后续导入同名模块时会直接使用缓存,导致test2实际复用了test1的custom_module和Generic_named。
方法1:导入前清除模块缓存
在每个测试脚本导入目标模块前,删除sys.modules中对应的模块条目,强制重新导入最新版本。
修改后的test1.py
import unittest import os import sys import shutil if os.path.exists('Tests/temp/1'): shutil.rmtree('Tests/temp/1') shutil.copytree('Tests/mock/1', 'Tests/temp/1/') shutil.copyfile('Programs/program1/Generic_named.py', 'Tests/temp/1/Generic_named.py') # 清除已缓存的同名模块 for module_name in ['Generic_named', 'custom_module']: if module_name in sys.modules: del sys.modules[module_name] sys.path.append('Tests/temp/1') import Generic_named sys.path.remove('Tests/temp/1') class Test(unittest.TestCase): def test_case1(self): self.assertEqual(Generic_named.main(), "Hello, World!") if __name__ == '__main__': unittest.main()
修改后的test2.py
import unittest import os import sys import shutil if os.path.exists('Tests/temp/2'): shutil.rmtree('Tests/temp/2') shutil.copytree('Tests/mock/2', 'Tests/temp/2') shutil.copyfile('Programs/program2/Generic_named.py', 'Tests/temp/2/Generic_named.py') # 清除已缓存的同名模块 for module_name in ['Generic_named', 'custom_module']: if module_name in sys.modules: del sys.modules[module_name] sys.path.append('Tests/temp/2') import Generic_named sys.path.remove('Tests/temp/2') class Test(unittest.TestCase): def test_case1(self): self.assertEqual(Generic_named.main(), "Goodbye, World!") if __name__ == '__main__': unittest.main()
方法2:用unittest的setUp/tearDown隔离测试环境
把环境准备、模块导入放到setUp方法,缓存清理放到tearDown,确保每个测试用例拥有独立的模块环境。
修改后的test1.py
import unittest import os import sys import shutil class Test(unittest.TestCase): def setUp(self): # 准备临时目录 if os.path.exists('Tests/temp/1'): shutil.rmtree('Tests/temp/1') shutil.copytree('Tests/mock/1', 'Tests/temp/1/') shutil.copyfile('Programs/program1/Generic_named.py', 'Tests/temp/1/Generic_named.py') # 清除模块缓存 for module_name in ['Generic_named', 'custom_module']: if module_name in sys.modules: del sys.modules[module_name] # 导入目标模块 sys.path.append('Tests/temp/1') global Generic_named import Generic_named sys.path.remove('Tests/temp/1') def tearDown(self): # 清理临时目录和模块缓存 shutil.rmtree('Tests/temp/1') for module_name in ['Generic_named', 'custom_module']: if module_name in sys.modules: del sys.modules[module_name] def test_case1(self): self.assertEqual(Generic_named.main(), "Hello, World!") if __name__ == '__main__': unittest.main()
test2.py做对应修改(替换路径编号为2即可)。
方法3:用importlib动态加载模块
通过importlib.util动态加载模块,给每个测试的模块分配唯一名称,绕开缓存冲突。
修改后的test1.py
import unittest import os import sys import shutil import importlib.util class Test(unittest.TestCase): def test_case1(self): # 准备临时环境 if os.path.exists('Tests/temp/1'): shutil.rmtree('Tests/temp/1') shutil.copytree('Tests/mock/1', 'Tests/temp/1/') shutil.copyfile('Programs/program1/Generic_named.py', 'Tests/temp/1/Generic_named.py') # 动态加载模块,指定唯一名称避免缓存冲突 module_path = os.path.join('Tests/temp/1', 'Generic_named.py') spec = importlib.util.spec_from_file_location('Generic_named_test1', module_path) Generic_named = importlib.util.module_from_spec(spec) sys.modules['Generic_named_test1'] = Generic_named spec.loader.exec_module(Generic_named) # 执行测试 self.assertEqual(Generic_named.main(), "Hello, World!") # 清理临时目录 shutil.rmtree('Tests/temp/1') if __name__ == '__main__': unittest.main()
test2.py中把模块名称改为Generic_named_test2,路径替换为Tests/temp/2即可。
内容的提问来源于stack exchange,提问作者Programmer S

