在Python中,如何从另一个未本地导入的文件中修补功能?


问题内容

我正在学习Pythonic测试开发,偶然发现了这个看似反直觉的问题。当我修补与被测代码在同一文件中定义的函数时,该函数patch可以正常工作。但是,当我import使用其他文件中的函数时,唯一能够patch正常工作的方法是将import本地化,而不是在文件顶部定义它。

最小复制量:

a / b.py:

from x.y import z


def c():
    print("In a.b.c")


class D:
    def do_stuff_with_a_b_c(self):
        print("In do_stuff_with_a_b_c")
        c()

    def do_stuff_with_x_y_z(self):
        from x.y import z
        print("In do_stuff_with_x_y_z")
        z()

x / y.py:

def z():
    print("In x.y.z")

测试/d_tests.py:

import inspect
import unittest
from unittest.mock import patch

from x import y
from a.b import D


class DTests(unittest.TestCase):
    def test_do_stuff_with_a_b_c(self):
        print(f"In {inspect.stack()[0][3]}")
        D().do_stuff_with_a_b_c()

    @patch("a.b.c")
    def test_do_stuff_with_patched_a_b_c(self, a_b_c_method):
        print(f"In {inspect.stack()[0][3]}")
        D().do_stuff_with_a_b_c()

    def test_do_stuff_with_x_y_z(self):
        print(f"In {inspect.stack()[0][3]}")
        D().do_stuff_with_x_y_z()

    @patch("x.y.z")
    def test_do_stuff_with_patched_x_y_z(self, x_y_z_method):
        print(f"In {inspect.stack()[0][3]}")
        D().do_stuff_with_x_y_z()

    def test_do_stuff_with_patch_object_x_y_z(self):
        print(f"In {inspect.stack()[0][3]}")
        with patch.object(y, "z"):
            D().do_stuff_with_x_y_z()


if __name__ == '__main__':
    unittest.main()

当我使用上面的代码运行测试时,得到以下输出:

In test_do_stuff_with_a_b_c
In do_stuff_with_a_b_c
In a.b.c
In test_do_stuff_with_patch_object_x_y_z
In do_stuff_with_x_y_z
In test_do_stuff_with_patched_a_b_c
In do_stuff_with_a_b_c
In test_do_stuff_with_patched_x_y_z
In do_stuff_with_x_y_z
In test_do_stuff_with_x_y_z
In do_stuff_with_x_y_z
In x.y.z

但是,当我注释掉x.y.zin的本地导入时do_stuff_with_x_y_z,我得到以下输出:

In test_do_stuff_with_a_b_c
In do_stuff_with_a_b_c
In a.b.c
In test_do_stuff_with_patch_object_x_y_z
In do_stuff_with_x_y_z
In x.y.z
In test_do_stuff_with_patched_a_b_c
In do_stuff_with_a_b_c
In test_do_stuff_with_patched_x_y_z
In do_stuff_with_x_y_z
In x.y.z

导致patch在一种情况下按预期工作但在另一种情况下无法工作的两种形式之间有什么区别?


问题答案:

修补时,顶级和本地导入这两种情况必须以不同的方式处理。您必须按照文档中或Ned
Batchelder的本博文中的描述对模块中使用的对象进行修补,后者将更详细地描述问题。

在第一种情况下,您需要在修补模块 z在运行时导入模块,以便导入修补后的模块。在第二种情况(顶级导入的更标准的情况)中,您 修补模块
之前先 导入模块,现在在模块中已有未修补的引用,因此要使修补起作用,您必须修补 此引用 z``b __

@patch('a.b.z')
def test_do_stuff_with_patched_x_y_z(self, mocked_z):
   ...

总结一下:您始终必须检查要使用的对象如何在要测试的模块中导入。基本上有两种情况:

  • 该对象的导入方式为import ximport x.y(和使用方式类似x.y.z)-在这种情况下,可以将其修补为x.y.z。如果像在最初的情况那样在函数内部本地导入模块,也是如此。
  • from x.y import z(或from .y import z)一样导入对象-在这种情况下,将创建引用该对象的本地引用,并且应对该引用进行修补(例如,a.b.z在您的情况下)