Python高级技术之:`Python`的`mocking`:`unittest.mock`和`pytest-mock`在单元测试中的应用。

各位靓仔靓女们,晚上好!今天咱来聊聊Python单元测试里一个很实用、但也容易让人头大的话题:Mocking。别怕,保证咱用最接地气的方式,把unittest.mockpytest-mock这两个好伙伴给盘明白。

开场白:别让外部依赖拖你后腿

想象一下,你写了一个超牛的函数,功能强大,逻辑清晰。但是,它需要连接数据库,或者调用一个外部API。问题来了:

  • 数据库挂了怎么办? 测试总是连不上数据库,或者数据库里没数据,测试就过不了。
  • API收费了怎么办? 免费API突然要收费,或者API每天调用次数有限制,测试总失败。
  • 外部服务不稳定怎么办? 网络不稳定,外部服务时好时坏,测试结果忽上忽下,让人怀疑人生。

这些外部依赖就像绊脚石,让你的单元测试寸步难行。这时候,Mocking就该闪亮登场了!

什么是Mocking?

简单来说,Mocking就是用“假货”代替“真货”。在单元测试中,我们用Mock对象来模拟外部依赖的行为,让你的函数只关注自己的核心逻辑,不受外部因素的干扰。

unittest.mock:Python自带的Mock神器

Python标准库自带了unittest.mock模块,提供了强大的Mocking功能。咱们先从它入手,一步一步来。

  1. Mock类:最基础的模拟对象

    Mock类可以模拟任何对象,包括函数、类、模块,甚至属性。

    from unittest.mock import Mock
    
    # 模拟一个函数
    my_func = Mock(return_value=42)
    result = my_func(1, 2, key='value')
    
    print(result)          # 输出: 42
    print(my_func.called)  # 输出: True
    print(my_func.call_args) # 输出: ((1, 2), {'key': 'value'})
    print(my_func.call_count) # 输出: 1
    • return_value:指定Mock对象的返回值。
    • called:判断Mock对象是否被调用过。
    • call_args:获取Mock对象的调用参数(位置参数和关键字参数)。
    • call_count:获取Mock对象的调用次数。

    是不是很简单? 你可以给Mock对象设置各种属性,模拟各种行为。

  2. patch装饰器:替换真实对象

    patch装饰器可以临时替换某个对象,在测试结束后自动恢复。这简直是单元测试的福音!

    from unittest.mock import patch
    
    def get_data_from_api():
       # 假设这个函数调用了外部API
       import requests
       response = requests.get('https://api.example.com/data')
       return response.json()
    
    @patch('__main__.requests.get') # 注意这里的路径 '__main__.requests.get'
    def test_get_data_from_api(mock_get):
       # 配置Mock对象的行为
       mock_get.return_value.json.return_value = {'data': 'mocked data'}
    
       # 调用被测试的函数
       data = get_data_from_api()
    
       # 断言
       assert data == {'data': 'mocked data'}
       mock_get.assert_called_once_with('https://api.example.com/data')
    • @patch('__main__.requests.get')patch装饰器接受一个字符串参数,指定要替换的对象。注意这里的路径需要写清楚,包括模块名和函数名。__main__是当前模块的名字。
    • mock_getpatch装饰器会将Mock对象作为参数传递给测试函数。
    • mock_get.return_value.json.return_value = {'data': 'mocked data'}:模拟API返回的JSON数据。注意,这里需要一层一层地设置返回值,直到到达最终的返回值。
    • mock_get.assert_called_once_with('https://api.example.com/data'):断言requests.get函数被调用了一次,并且参数是'https://api.example.com/data'

    patch装饰器还有很多用法,比如可以替换类的属性、模块中的变量等等。

  3. side_effect:模拟更复杂的行为

    side_effect可以让你模拟更复杂的行为,比如抛出异常、返回不同的值等等。

    from unittest.mock import Mock
    
    def my_func(x):
       if x > 0:
           return "Positive"
       elif x < 0:
           return "Negative"
       else:
           return "Zero"
    
    mock_func = Mock(side_effect=my_func)
    
    print(mock_func(1))  # 输出: Positive
    print(mock_func(-1)) # 输出: Negative
    print(mock_func(0))  # 输出: Zero
    
    # 模拟抛出异常
    def raise_exception(*args, **kwargs):
       raise ValueError("Something went wrong!")
    
    mock_func = Mock(side_effect=raise_exception)
    
    try:
       mock_func(1)
    except ValueError as e:
       print(e) # 输出: Something went wrong!
    • side_effect可以是一个函数,每次调用Mock对象时,都会执行这个函数,并返回它的返回值。
    • side_effect也可以是一个异常,每次调用Mock对象时,都会抛出这个异常。
    • side_effect还可以是一个迭代器,每次调用Mock对象时,都会返回迭代器的下一个值。

pytest-mock:Pytest的Mock好帮手

pytest-mock是一个pytest插件,它封装了unittest.mock,提供了更简洁、更方便的Mocking API。

  1. mocker fixture:pytest的Mock对象

    pytest-mock提供了一个名为mocker的fixture,你可以直接在测试函数中使用它。

    import pytest
    from unittest.mock import Mock
    
    def my_func():
       # 假设这个函数调用了外部函数
       return external_func()
    
    def external_func():
       # 这是外部函数,我们需要mock它
       return "Real data"
    
    def test_my_func(mocker):
       # 创建Mock对象
       mock_external_func = mocker.patch('__main__.external_func', return_value="Mocked data")
    
       # 调用被测试的函数
       result = my_func()
    
       # 断言
       assert result == "Mocked data"
       mock_external_func.assert_called_once()
    • mocker.patch('__main__.external_func', return_value="Mocked data"):使用mocker.patch方法替换external_func函数,并设置返回值。
    • mock_external_func.assert_called_once():断言external_func函数被调用了一次。

    pytest-mock还提供了其他一些方便的方法,比如:

    • mocker.spy():可以监视函数的调用情况,但不会替换它。
    • mocker.stub():创建一个简单的占位符对象。
  2. mocker.patch.object:Mock对象的属性
    针对类的属性进行Mock,通常会使用mocker.patch.object

    import pytest
    from unittest.mock import Mock
    
    class MyClass:
       def __init__(self, value):
           self.value = value
    
       def get_value(self):
           return self.value
    
    def test_get_value(mocker):
       instance = MyClass(10)
       mocker.patch.object(instance, 'value', new_value=20)
       assert instance.get_value() == 20

    这个例子中, mocker.patch.object被用来修改MyClass实例的value属性。

Mocking 的最佳实践

  1. 只Mock你需要的:不要过度Mock。只Mock那些影响你的核心逻辑的外部依赖。
  2. 保持测试的简洁:Mock代码应该清晰易懂,不要让Mock代码比被测试的代码还复杂。
  3. 验证Mock对象的行为:确保你的Mock对象被正确地调用,并且返回了正确的值。
  4. 使用上下文管理器:对于复杂的Mock场景,可以使用with语句来管理Mock对象的生命周期。

总结:Mocking让单元测试更靠谱

功能 unittest.mock pytest-mock 优点 缺点
核心类/方法 Mock, patch, side_effect mocker.patch, mocker.spy, mocker.stub Python自带,无需安装;功能强大,灵活性高 语法相对繁琐
集成 unittest框架集成 pytest框架集成 pytest无缝集成,使用方便;语法更简洁 需要安装;相比unittest.mock,灵活性稍差
使用场景 适用于任何Python项目,尤其是使用unittest框架的项目 适用于使用pytest框架的项目,追求简洁、方便的Mocking方式

Mocking是单元测试中不可或缺的一部分。它可以让你隔离外部依赖,专注于测试核心逻辑,提高测试的可靠性和效率。无论是unittest.mock还是pytest-mock,都是非常优秀的Mocking工具。选择哪个取决于你的项目和个人偏好。

记住,Mocking的目的是让你的测试更简单、更可靠。不要让Mocking本身成为负担。希望今天的分享能帮助你更好地掌握Mocking技术,写出更棒的Python代码!

今天就到这里,各位晚安!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注