程序可运行但单元测试失败?全局变量作用域问题排查
问题分析与解决方案
问题描述
需要为一段依赖模块级全局变量的Python代码编写单元测试,原程序可正常运行,但测试执行时提示globTools is not defined。
待测试代码(简化版)
from services import myModule1 from services.spec1 import importedFunc from services.spec2 import getTool from services.spec3 import getDict class myClass(object): def __init__(self, param1, param2): self.param1 = param1 self.param2 = param2 self.param3 = 0 self.param4 = 0 def myMethod(self): try: myVar1 = globalDict['key1'] myVar2 = globalDict['key2'] newVar = importedFunc(par1=myVar1, par2=myVar2, par3=extVar3) calcParam = myModule1.methodMod1(self.param1) self.param3 = calcParam["keyParam3"] self.param4 = newVar.meth1(self.param2) globTools.send_message(self.param3, self.param4) except: globTools.error_message(self.param3, self.param4) return class myClass2(object): def __init__(self, *myclass2_params): # 初始化专属属性的代码 self.add_objects() def add_objects(self): # 其他代码 my_class = myClass(**necessary_params) # 其他代码 return if __name__ == '__main__': globTools = getTool("my_program") globalDict = getDict(some_params) # 其他代码 my_class2 = myClass2(**any_params) # 其他代码
测试代码(初始版本)
import unittest from my_module import myClass from services import myModule1 from services.spec1 import importedFunc from services.spec2 import getTool from services.spec3 import getDict def test_myClass(unittest.TestCase): def setUp(self): globTools = getTool("my_program") globalDict = getDict(some_params) def test_myMethod(self): test_class = myClass(*necessary_parameters) test_res = test_class.myMethod() self.assertIsNotNone(test_res) if __name__ == '__main__': unittest.main()
原因解析
- 作用域差异:原程序运行时,
globTools、globalDict是在模块的全局作用域(if __name__ == '__main__'代码块)中定义的,myClass的方法与这些变量同属一个模块,因此可以直接访问。但测试运行时,my_module是作为被导入的模块存在的,测试代码中定义的变量属于测试模块的作用域,无法进入my_module的全局命名空间,导致myMethod找不到依赖的全局变量。 - 测试类写法错误:初始测试代码将测试用例定义为函数
def test_myClass(unittest.TestCase),但unittest框架要求测试用例必须是继承自unittest.TestCase的类,这种写法会导致框架无法正确识别测试逻辑。
修复方案
方案一:手动注入模块级全局变量
直接在测试中把依赖的全局变量注入到my_module的模块命名空间中,模拟原程序运行时的环境:
import unittest import my_module # 直接导入整个模块,而非仅导入类 from services.spec2 import getTool from services.spec3 import getDict class TestMyClass(unittest.TestCase): def setUp(self): # 将全局变量注入到my_module的模块作用域 my_module.globTools = getTool("my_program") my_module.globalDict = getDict(some_params) my_module.extVar3 = "test_ext_value" # 补充myMethod依赖的extVar3变量 def test_myMethod(self): test_class = my_module.myClass(param1="test_p1", param2="test_p2") test_res = test_class.myMethod() # 注意原myMethod的return是None,这里修正断言逻辑 self.assertIsNone(test_res) if __name__ == '__main__': unittest.main()
方案二:使用unittest.mock模拟依赖(推荐)
通过mock工具模拟所有外部依赖,避免依赖真实服务,同时能精准验证方法的调用逻辑:
import unittest from unittest.mock import patch, MagicMock from my_module import myClass class TestMyClass(unittest.TestCase): def setUp(self): # 预定义所有mock对象的行为 self.mock_globTools = MagicMock() self.mock_globalDict = {"key1": "test_key1", "key2": "test_key2"} self.mock_extVar3 = "test_ext_var" # 模拟importedFunc返回的对象及meth1方法的返回值 self.mock_newVar = MagicMock() self.mock_newVar.meth1.return_value = 100 # 模拟myModule1.methodMod1的返回值 self.mock_calc_param = {"keyParam3": 200} @patch('my_module.globTools') @patch('my_module.globalDict') @patch('my_module.extVar3') @patch('my_module.importedFunc') @patch('my_module.myModule1.methodMod1') def test_myMethod(self, mock_method_mod1, mock_imported_func, mock_ext_var3, mock_global_dict, mock_glob_tools): # 为mock对象赋值 mock_glob_tools = self.mock_globTools mock_global_dict = self.mock_globalDict mock_ext_var3 = self.mock_extVar3 mock_imported_func.return_value = self.mock_newVar mock_method_mod1.return_value = self.mock_calc_param # 执行测试 test_class = myClass(param1="test_p1", param2="test_p2") test_res = test_class.myMethod() # 验证方法调用是否符合预期 mock_imported_func.assert_called_once_with(par1="test_key1", par2="test_key2", par3="test_ext_var") mock_method_mod1.assert_called_once_with("test_p1") self.mock_newVar.meth1.assert_called_once_with("test_p2") self.mock_globTools.send_message.assert_called_once_with(200, 100) self.assertIsNone(test_res) if __name__ == '__main__': unittest.main()
内容的提问来源于stack exchange,提问作者Christophe
相关产品推荐
相关产品推荐

