Python、单元测试与模拟导入

15 投票
5 回答
5338 浏览
提问于 2025-04-11 09:25

我正在一个项目中,开始重构一些庞大的代码。遇到的一个问题是,每个文件都引入了很多其他文件。我想知道怎么优雅地在单元测试中模拟这些引入,而不需要改动实际代码,这样我就可以开始写单元测试了。

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

我希望能够尽可能独立地运行单元测试,目前我只打算测试那些不依赖于被引入文件内容的函数。

编辑

感谢大家的回答。

一开始我其实不知道自己想做什么,但现在我觉得我明白了。

问题在于,有些引入只有在整个应用程序运行时才能使用,因为有一些第三方的自动化功能。所以我不得不在一个目录中为这些模块创建一些占位符,并通过sys.path指向它。

现在我可以在我的单元测试文件中引入包含我想写测试的函数的文件,而不会出现缺少模块的错误。

5 个回答

1

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

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的库吗?如果会,那你得做很多工作。如果不会,那你可能只需要模拟那些你确实要测试的部分。

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 的第二个参数是你想在内部模块中导入的模块名称列表。

这个例子还可以进一步修改,比如导入一个你不想要的其他模块,或者甚至用你自己定义的对象来替代模块对象。

撰写回答