在Python中,一个函数被另一个函数调用的模拟补丁

每当我们编写健壮的代码时,单元测试是必不可少的。它们帮助我们验证我们的应用逻辑并检查代码要求的所有其他方面;然而,像复杂性和外部依赖性这样的障碍使我们很难编写高质量的测试案例。

在这一点上,unittest.mock ,一个Python模拟库,帮助我们克服这些障碍。今天,我们将学习模拟对象,它们的重要性,以及使用Pythonpatch() 的各种示例代码,它将暂时用模拟对象替换目标。

模拟对象和它们的重要性

模拟对象以可控的方式模仿真实对象的行为,例如,在测试环境中,我们进行不同的测试以确保代码按预期工作。使用模拟对象的过程被称为Mocking;它是一个强大的工具,可以提高我们测试的质量。

现在,重点是,我们为什么要通过嘲讽?使用它的根本原因是什么?

各种原因表明了在Python中使用mocking的重要性。让我们看一下下面的内容:

  1. 一个主要原因是通过使用模拟对象来改善我们代码的行为。例如,我们可以用它们来测试HTTP请求(由客户端向命名的主机发出的请求,该主机位于服务器上),并确保它们是否会导致任何间歇性故障。
  2. 我们可以用模拟对象代替实际的HTTP请求,这样我们就可以伪造一个外部服务的中断,并以一种可预测的方式完全成功地响应。
  3. 当难以测试if 语句和except 块时,嘲讽也很有用。使用模拟对象,我们可以控制代码的执行流程,以达到这样的区域(ifexcept),并提高代码覆盖率。
  4. 另一个增加使用Python模拟对象的重要性的原因是正确理解我们如何在代码中使用它们的对应物。

PythonPatch() 和它的用途

unittest.mock ,是Python中的一个模拟对象库,它有一个patch() ,可以用模拟对象暂时替换目标。这里,目标可以是一个类,一个方法,或者一个函数。

为了使用这个补丁,我们需要了解如何识别目标并调用patch() 函数。

要识别目标,确保目标是可导入的,然后在目标被利用的地方打上补丁,而不是在它的来源地。我们可以通过三种方式调用patch() ;作为一个类/函数的装饰器,作为一个上下文管理器,或者作为一个手动启动/停止。

当我们使用patch() 作为类/函数的装饰器或在with 语句内的上下文管理器中使用patch() 时,目标被替换为新对象。在这两种情况下,当with 语句或函数存在时,补丁会被撤销。

让我们创建一个启动代码并将其保存在addition.py 文件中,我们将导入该文件以使用patch() 作为装饰器、上下文管理器和手动启动/停止。

启动示例代码(保存在addition.py 文件中):

def read_file(filename):
    with open(filename) as file:
        lines = file.readlines()
        return [float(line.strip()) for line in lines]
def calculate_sum(filename):
    numbers = read_file(filename)
    return sum(numbers)
filename = "./test.txt"
calculate_sum(filename)

test.txt 的内容:

1
2
3

read_file() 以 读取行数,并将每一行转换为浮点数类型。它返回这些转换后的数字的列表,我们将其保存在 函数内的 变量中。filename calculate_sum() numbers

现在,calculate_sum()numbers 列表中的所有数字相加,并将其作为输出返回,如下所示:

OUTPUT:

6.0

patch() 作为一个装饰器

让我们一步步深入学习patch() 作为装饰器的使用。在所有步骤的末尾都给出了完整的源代码。

  • 导入库和模块。
    import unittest
    from unittest.mock import patch
    import addition
    

    首先,我们导入unittest 库,然后从unittest.mock 模块导入patch 。之后,我们导入addition, 我们的启动代码。

  • 装饰test_calculate_sum() 方法。
    @patch('addition.read_file')
      def test_calculate_sum(self, mock_read_file):
    	  #....
    

    接下来,我们使用@patch 装饰器来装饰test_calculate_sum() 测试方法。这里,目标是addition 模块的read_file() 函数。

    由于使用了@patch 装饰器,test_calculate_sum() 有一个额外的参数mock_read_file 。这个额外的参数是MagicMock 的一个实例(我们可以把mock_read_file 改名为我们想要的任何东西)。

    test_calculate_sum() 中,patch()mock_read_file 对象替换了addition.read_file() 函数。

  • mock_read_file.return_value 赋值一个列表。
    mock_read_file.return_value = [1, 2, 3]
    
  • 调用calculate_sum() ,并测试它。
    result = addition.calculate_sum('')
    self.assertEqual(result, 6.0)
    

    现在,我们可以调用calculate_sum() 并使用assertEqual() 来测试和是否是6.0

    calculate_sum() 在这里,我们可以将任何filename ,因为mock_read_file 对象将被调用,而不是addition.read_file() 函数。

  • 下面是完整的源代码。

    示例代码(保存在test_sum_patch_decorator.py 文件中)

    import unittest
    from unittest.mock import patch
    import addition
    class TestSum(unittest.TestCase):
      @patch('addition.read_file')
      def test_calculate_sum(self, mock_read_file):
    	  mock_read_file.return_value = [1, 2, 3]
    	  result = addition.calculate_sum('')
    	  self.assertEqual(result, 6.0)
    

    运行一个测试:

    python -m unittest test_sum_patch_decorator -v
    

    使用上述命令运行测试,得到以下输出。

    OUTPUT:

    test_calculate_sum (test_sum_patch_decorator.TestSum) ... ok
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    OK
    

使用patch() 作为上下文管理器

示例代码(保存在test_sum_patch_context_manager.py 文件中):

import unittest
from unittest.mock import patch
import addition
class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        with patch('addition.read_file') as mock_read_file:
            mock_read_file.return_value = [1, 2, 3]
            result = addition.calculate_sum('')
            self.assertEqual(result, 6)

这段代码类似于我们使用patch() 作为装饰器的上一个代码例子,除了这里讨论的一些区别。

现在,我们没有 @patch('addition.read_file') 行代码,而test_calculate_sum() 只接受self 参数(这是一个默认参数)。

with patch('addition.read_file') as mock_read_file 意味着使用上下文管理器中的mock_read_file 对象来修补addition.read_file()

简单地说,我们可以说patch() 将在with 块中用mock_read_file 对象替换addition.read_file()

现在,使用下面的命令运行测试。

python -m unittest test_sum_patch_context_manager -v

OUTPUT:

test_calculate_sum (test_sum_patch_context_manager.TestSum) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK

使用patch() 来手动启动/停止

示例代码(保存在test_sum_patch_manually.py 文件中):

import unittest
from unittest.mock import patch
import addition
class TestSum(unittest.TestCase):
    def test_calculate_sum(self):
        patcher = patch('addition.read_file')
        mock_read_file = patcher.start()
        mock_read_file.return_value = [1, 2, 3]
        result = addition.calculate_sum('')
        self.assertEqual(result, 6.0)
        patcher.stop()

这个代码栅栏的做法与前面两个代码例子相同,但在这里,我们手动使用patch() 。怎么做呢?下面我们来了解一下。

首先,我们导入所需的库。在test_calculate_sum() ,我们调用patch() ,开始与addition 模块的目标read_file() 函数进行修补。

然后,我们为read_file() 函数创建一个模拟对象。之后,将数字列表分配给mock_read_file.return_value ,调用calculate_sum() ,用assertEqual() 测试结果。

最后,我们通过调用patcher 对象的stop() 方法停止修补。现在,运行以下命令进行测试。

python -m unittest test_sum_patch_manually -v

OUTPUT:

test_calculate_sum (test_sum_patch_manually.TestSum) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK