Python:拦截类加载动作

时间:2009-12-23 00:29:56

标签: python unit-testing

总结:当导入某个python模块时,我希望能够拦截这个动作,而不是加载所需的类,我想加载我选择的另一个类。

原因:我正在研究一些遗留代码。在开始一些增强/重构之前,我需要编写一些单元测试代码。但是,代码会导入某个模块,该模块在单元测试设置中会失败。 (由于数据库服务器依赖性)

Pseduo代码:

from LegacyDataLoader import load_me_data
...
def do_something():
   data = load_me_data()

因此,理想情况下,当python在单元测试中使用上面的导入行时,会加载替代类MockDataLoader。

我仍在使用2.4.3。我想有一个我可以操作的导入钩子

修改

到目前为止,非常感谢您的答案。他们都非常有帮助。

一种特殊类型的建议是关于操纵PYTHONPATH。它在我的情况下不起作用。所以我将在这里阐述我的具体情况。

原始代码库以这种方式组织

./dir1/myapp/database/LegacyDataLoader.py
./dir1/myapp/database/Other.py
./dir1/myapp/database/__init__.py
./dir1/myapp/__init__.py

我的目标是在Other模块中增强Other类。但是因为它是遗留代码,所以如果不首先将测试套件捆绑在一起,我会感到很自在。

现在我介绍一下这个单元测试代码

./unit_test/test.py

内容很简单:

from myapp.database.Other import Other

def test1():
            o = Other()
            o.do_something()

if __name__ == "__main__":
            test1()

当CI服务器运行上述测试时,测试失败。这是因为类Other使用LegacyDataLoader,而LegacydataLoader无法从CI框建立与数据库服务器的数据库连接。

现在让我们按照建议添加假类:

./unit_test_fake/myapp/database/LegacyDataLoader.py
./unit_test_fake/myapp/database/__init__.py
./unit_test_fake/myapp/__init__.py

将PYTHONPATH修改为

export PYTHONPATH=unit_test_fake:dir1:unit_test

现在测试因其他原因而失败

  File "unit_test/test.py", line 1, in <module>
    from myapp.database.Other import Other
ImportError: No module named Other

它与python解析模块中的类/属性的方式有关

4 个答案:

答案 0 :(得分:3)

您可以通过定义自己的import函数并将其分配给from ... import来拦截__import____builtin__.__import__语句(确保保存以前的值,因为您的覆盖将会毫无疑问,我想委托它;你需要import __builtin__来获取内置对象模块。

例如(具体是Py2.4,因为这就是你要问的内容),请在aim.py中保存以下内容:

import __builtin__
realimp = __builtin__.__import__
def my_import(name, globals={}, locals={}, fromlist=[]):
  print 'importing', name, fromlist
  return realimp(name, globals, locals, fromlist)
__builtin__.__import__ = my_import

from os import path

现在:

$ python2.4 aim.py
importing os ('path',)

因此,您可以拦截所需的任何特定导入请求,并在返回之前根据需要更改导入的模块[s] - 请参阅规范here。这是你正在寻找的那种“钩子”,对吗?

答案 1 :(得分:1)

好吧,如果通过引发异常导致导入失败,你可以把它放在try ... except循环:

try:
    from LegacyDataLoader import load_me_data
except: # put error that occurs here, so as not to mask actual problems
    from MockDataLoader import load_me_data

这就是你要找的东西吗?如果它失败了,但没有引发异常,你可以使用特殊的命令行标记来运行单元测试,比如--unittest,如下所示:

import sys
if "--unittest" in sys.argv:
    from MockDataLoader import load_me_data
else:
    from LegacyDataLoader import load_me_data

答案 2 :(得分:1)

有更简洁的方法可以执行此操作,但我认为您无法修改包含from LegacyDataLoader import load_me_data的文件。

最简单的方法是创建一个名为testing_shims的新目录,并在其中创建LegacyDataLoader.py文件。在该文件中,定义您喜欢的假load_me_data。运行单元测试时,将testing_shims作为第一个目录放入PYTHONPATH环境变量中。或者,您可以修改测试运行器以将testing_shims作为sys.path中的第一个值插入。

这样,导入LegacyDataLoader时会找到你的文件,你的代码将被加载而不是真正的代码。

答案 3 :(得分:1)

如果在那里找到匹配的名称,import语句只会从sys.modules中获取东西,所以最简单的方法是确保在之前将目标名称​​下的自己的模块插入到sys.modules中其他任何东西都试图导入真实的东西。

# in test code
import sys
import MockDataLoader
sys.modules['LegacyDataLoader'] = MockDataLoader

import module_under_test

主题有一些变化,但这种基本方法可以很好地完成您在问题中描述的内容。一个稍微简单的方法就是这样,只使用模拟函数来替换有问题的函数:

# in test code
import module_under_test

def mock_load_me_data():
    # do mock stuff here

module_under_test.load_me_data = mock_load_me_data

这只是简单地在模块本身中替换了相应的名称,所以当您调用测试中的代码时,大概是do_something()在你的问题中,它会调用你的模拟例程。