Python,单元测试和模拟导入

15

我所在的项目正在开始重构一些庞大的代码库。立即出现的一个问题是,每个文件都导入了许多其他文件。我该如何以一种优雅的方式在我的单元测试中模拟它,而无需修改实际代码,以便我可以开始编写单元测试呢?

例如:我想要测试的函数文件导入了十个其他的文件,这些文件是我们软件的一部分,而不是Python核心库。

我希望能够尽可能地独立运行单元测试,现在我只打算测试不依赖于导入的文件的函数。

编辑

感谢所有的回答。

一开始我不太清楚我想做什么,但现在我想我知道了。

问题在于某些导入只有在整个应用程序运行时才可能发生,因为涉及到一些第三方自动魔法。因此,我必须为这些模块制作一些存根,并将其放置在我指定的目录中,这个目录我使用了sys.path。

现在,我可以在我的单元测试文件中导入包含我要编写测试的函数的文件,而不会出现有关缺少模块的投诉。

5个回答

8

如果您想在导入模块的同时确保它不会导入任何其他内容,您可以替换__import__内置函数。

例如,使用以下类:

class ImportWrapper(object):
    def __init__(self, real_import):
        self.real_import = real_import

    def wrapper(self, wantedModules):
        def inner(moduleName, *args, **kwargs):
            if moduleName in wantedModules:
                print "IMPORTING MODULE", moduleName
                self.real_import(*args, **kwargs)
            else:
                print "NOT IMPORTING MODULE", moduleName
        return inner

    def mock_import(self, moduleName, wantedModules):
        __builtins__.__import__ = self.wrapper(wantedModules)
        try:
            __import__(moduleName, globals(), locals(), [], -1)
        finally:
            __builtins__.__import__ = self.real_import

在你的测试代码中,不要写 import myModule,而是写:

wrapper = ImportWrapper(__import__)
wrapper.mock_import('myModule', [])
mock_import的第二个参数是一个模块名列表,表示你需要在内部模块中导入的模块。
此示例可以进一步修改,例如导入其他模块而不是所需模块,甚至使用自己的自定义对象来模拟模块对象。

你想在mock_import方法上添加try: finally:,以避免在出现错误的情况下将系统留在包装后的导入状态而不是默认状态。 - Yonatan

1
“导入了很多其他文件”?是导入了许多其他文件,这些文件是您定制代码库的一部分吗?还是导入了许多其他文件,这些文件是Python发行版的一部分?或者是导入了许多其他开源项目文件?
如果您的导入不起作用,那么您可能遇到了一个“简单”的PYTHONPATH问题。将所有各种项目目录放到您可以用于测试的PYTHONPATH上。我们有一个相当复杂的路径,在Windows中,我们像这样管理它。
@set Part1=c:\blah\blah\blah
@set Part2=c:\some\other\path
@set that=g:\shared\stuff
set PYTHONPATH=%part1%;%part2%;%that%

我们将每个路径部分分开,这样我们就可以(a)知道东西来自哪里,(b)在移动东西时可以管理更改。

由于按顺序搜索PYTHONPATH,我们可以通过调整路径上的顺序来控制使用什么。

一旦你拥有了“一切”,问题就变成了信任的问题。

要么

  • 你信任某些东西(即Python代码库)并且只是导入它。

要么

  • 你不相信某些东西(即你自己的代码),你

    1. 单独测试它,并且
    2. 为独立测试而模拟它。

你会测试Python库吗?如果是,你需要做很多工作。如果不是,那么你应该只模拟你实际要测试的东西。


1
如果你真的想要深入了解Python的导入机制,可以看看ihooks模块。它提供了改变__import__内置函数行为的工具。但是从你的问题中并不清楚你为什么需要这样做。

0
在你的评论上面中,你说你想说服Python某些模块已经被导入了。这仍然似乎是一个奇怪的目标,但如果这确实是你想做的事情,原则上你可以绕过导入机制的背后,并更改sys.modules。不确定这对于包导入如何工作,但对于绝对导入应该没问题。

0

如果你想在单元测试之前快速解决问题,那么不需要进行复杂的操作。

如果单元测试与你要测试的代码在同一个文件中,只需从globals()字典中删除不需要的模块即可。

以下是一个相当冗长的示例:假设你有一个名为impp.py的模块,其内容如下:

value = 5

现在,在你的测试文件中,你可以写:

>>> import impp
>>> print globals().keys()
>>> def printVal():
>>>     print impp.value
['printVal', '__builtins__', '__file__', 'impp', '__name__', '__doc__']

请注意,impp是全局变量之一,因为它已被导入。调用使用impp模块的函数printVal仍然有效:
>>> printVal()
5

但是现在,如果您从globals()中删除 impp 键...

>>> del globals()['impp']
>>> print globals().keys()
['printVal', '__builtins__', '__file__', '__name__', '__doc__']

如果尝试调用printVal(),你会得到:

>>> printVal()
Traceback (most recent call last):
  File "test_imp.py", line 13, in <module>
    printVal()
  File "test_imp.py", line 5, in printVal
    print impp.value
NameError: global name 'impp' is not defined

这可能正是您想要实现的内容。

要在单元测试中使用它,您可以在运行测试套件之前删除全局变量,例如在__main__中:

if __name__ == '__main__':
    del globals()['impp']
    unittest.main()

谢谢您的回答,我从中学到了很多。但事实上,我想要做的恰恰相反。测试在另一个文件中,当我导入包含我想要测试的函数的文件时,我希望该文件认为所有其他文件都已经被导入。 - Rickard Lindroth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接