• python mock实践笔记


    前言

    如果你写代码时会写单元测试(unit test,UT),那么多半会遇到想要将某个函数隔离开,去掉外部依赖的情况,例如这个函数依赖其他函数的返回或者依赖某个API调用的返回。这种情况下就一定绕不开mock这项技术。本文并不打算介绍python下mock的方方面面,只会写我个人实际使用中觉得比较实用的部分。

    mocking是什么?

    mock就是模拟的意思,mocking主要用在单元测试中,当被测试的对象依赖另一个复杂的对象时,我们需要模拟这个复杂的对象的行为,mocking就是创建这个对象,模拟它的行为。

    基本用法

    unittest.mock模块包含了mock相关的功能。

    Mock对象

    看如下一段代码:

    from unittest.mock import Mock
    # 创建一个mock对象
    mock = Mock()
    print(mock)
    # 结果:<Mock id='140668914327224'> 表明此处的mock是一个Mock对象
    

    Mock非常灵活,当我们访问一个Mock对象的某个属性时,这个属性如果不存在会被自动创建,看如下代码:

    # 在访问之前mock并没有some_attribute这个属性
    print(mock.some_attribute)
    # 结果:<Mock name='mock.some_attribute' id='140348173848360'> 
    # 可见,在访问的时候创建了该属性
    
    print(mock.do_something)
    # 结果:<Mock name='mock.do_something' id='140348173886128'>
    

    正是由于此特性,Mock可以用来模拟任意对象。

    下面从最基本的开始介绍:

    设置返回值和属性

    Mock对象可以返回常量,也可以随着输入返回不同值。

    返回常量-return_value

    mock = Mock()
    # 设置返回值
    mock.return_value = 3
    print(mock()) # 返回 3
    # 设置方法的返回值
    mock.method.return_value = 3
    mock.method() # 返回 3
    # 在构造函数中设置返回值
    mock = Mock(return_value=3)
    mock() # 返回 3
    # 设置属性
    mock = Mock()
    mock.x = 3
    mock.x # 返回 3
    

    返回随着输入变化-side_effect

    side_effect也算一个属性,当你不满足指定一个常量返回时,就会期望用上它。

    # 1.将side_effect设置为一个异常类
    mock = Mock(side_effect=Exception('Boom!'))
    mock() # 调用时就会抛出异常
    
    from requests.exceptions import Timeout
    requests = Mock()
    requests.get.side_effect = Timeout # 模拟API超时
    with self.assertRaises(Timeout):
        # get_holidays函数里面调用了requests.get,那么将会捕获到Timeout异常
        get_holidays()
    # 将会抛出异常
    
    # 2.将side_effect设置为一个迭代器(场景:mock对象被多次调用,每次返回值不一样)
    mock = MagicMock(side_effect=[4, 5, 6])
    mock()
    4
    mock()
    5
    mock()
    6
    
    # 3.将side_effect设置为一个函数(场景:返回值由输入参数决定)
    vals = {(1, 2): 1, (2, 3): 2}
    def side_effect(*args):
        return vals[args]
    
    mock = MagicMock(side_effect=side_effect)
    mock(1, 2)
    1
    mock(2, 3)
    2
    
    

    mock一个类

    def some_function():
        instance = module.Foo()
        return instance.method()
    # 模拟Foo这个类
    with patch('module.Foo') as mock:
        # 此处的“mock”就是一个类,mock.return_value代表该类返回的实例(instance)
        instance = mock.return_value
        # 模拟实例方法的返回(此处的方法名就叫method)
        instance.method.return_value = 'the result'
        # 函数中对Foo的调用就会使用模拟类
        result = some_function()
        assert result == 'the result'
    

    模拟一个对象(object)的方法(method)

    patch

    patch可能是使用最多的方法,它的使用场景:

    1.模拟一个类的属性

    2.模拟一个模块的属性

    如果我们测试的函数在同一个文件中可以不使用patch,patch主要用在测试代码和主代码分离的情况下。

    有3种装饰器可用:

    # patch的第一个参数是一个string,形式:package.module.Class.attribute,以此指定要模拟的属性,第二个参数是可选的,第一个参数里面的属性将被替换为该值。
    @patch('package.module.attribute', sentinel.attribute)
    # 例子一,传有第二个参数:
    mock = MagicMock(return_value=sentinel.file_handle)
    with patch('builtins.open', mock):
        handle = open('filename', 'r')
    # 例子二,不传第二个参数:
    # 不传第二个参数时,mock对象会被传入在函数的参数里,如下,并且注意顺序:
    class MyTest(unittest.TestCase):
        @patch('package.module.ClassName1')
        @patch('package.module.ClassName2')
        def test_something(self, MockClass2, MockClass1):
            self.assertIs(package.module.ClassName1, MockClass1)
            self.assertIs(package.module.ClassName2, MockClass2)
    # 例子三,使用as,将会获得一个引用
    with patch('ProductionClass.method') as mock_method:
        mock_method.return_value = None
        real = ProductionClass()
        real.method(1, 2, 3)
    
    # 例子四,将path装饰在类上,作用于每个测试函数
    @patch('mymodule.SomeClass')
    class MyTest(unittest.TestCase):
    
        def test_one(self, MockSomeClass):
            self.assertIs(mymodule.SomeClass, MockSomeClass)
    
        def test_two(self, MockSomeClass):
            self.assertIs(mymodule.SomeClass, MockSomeClass)
    		# 装饰在类上只针对test开头的函数,此处不是test开头,不传递MockSomeClass参数
        def not_a_test(self):
            return 'something'
    
    # 例子五,另一种在整个类中模拟的办法
    class MyTest(unittest.TestCase):
        def setUp(self):
            patcher = patch('mymodule.foo')
            self.addCleanup(patcher.stop)
            self.mock_foo = patcher.start()
    
        def test_foo(self):
            self.assertIs(mymodule.foo, self.mock_foo)
    
    
    # patch的类在同一个文件中
    # 此处需要使用__main__,代表当前模块
    @patch('__main__.SomeClass')
    
    
    # patch.object第一个参数是一个对象,第二个参数是该对象的属性名称,第三个是可选的,第二个参数里面的属性将被替换为该值。
    # 场景:只模拟部分属性而非整个对象
    @patch.object(SomeClass, 'attribute', sentinel.attribute)
    
    @patch.dict()
    

    如下为例子,供参考、copy:

    # my_calendar.py
    import requests
    from datetime import datetime
    
    def is_weekday():
        today = datetime.today()
        # Python's datetime library treats Monday as 0 and Sunday as 6
        return (0 <= today.weekday() < 5)
    
    def get_holidays():
        r = requests.get('http://localhost/api/holidays')
        if r.status_code == 200:
            return r.json()
        return None
    
    # tests.py
    import unittest
    from my_calendar import get_holidays
    from requests.exceptions import Timeout
    from unittest.mock import patch
    
    class TestCalendar(unittest.TestCase):
        # patch装饰在函数上如果函数里面会调用my_calendar下的requests函数,就会被mock掉
        @patch('my_calendar.requests')
        def test_get_holidays_timeout(self, mock_requests):
                mock_requests.get.side_effect = Timeout
                with self.assertRaises(Timeout):
                    get_holidays()
                    mock_requests.get.assert_called_once()
    
    if __name__ == '__main__':
        unittest.main()
    

    mock可以直接使用装饰器,也可以使用上下文管理器,为什么使用上下文管理器?一般原因有如下两个,可自行判断要不要用:

    1.只想针对部分代码,而不是整个测试函数

    2.patch装饰器已经很多了,装饰器太多影响可读性

    patch路径应该是什么?

    where to patch?并不是要去引用某个函数本身所在的位置,而是要看这个函数在哪里使用的,如果在使用了的地方有import,那么就应该是那个地方的路径。
    举例:

    一个文件中(这个文件路径:package2.m2.py):

    from package1.m1 import fun1
    def fun2():
      fun1()
    

    在另一个测试文件中:

    class JustTest(TestCase):
      @patch('package2.m2.fun1') # 这才是正确的路径,而不是package1.m1.fun1
      def test_fun2(self, mock_fun1):
        mock_fun1.return_value = 3
    

    参考

    python官方示例

    How to: Unit testing in Django with mocking and patching

    What is Mocking?

    https://realpython.com/python-mock-library/

  • 相关阅读:
    numpy数组各种乘法
    python测试函数的使用时间
    dataframe 列名重新排序
    《图解设计模式》读书笔记5-1 composite模式
    《图解设计模式》读书笔记4-2 STRATEGY模式
    《图解设计模式》读书笔记4-1 Bridge模式
    《图解设计模式》读书笔记3-3 Builder模式
    《图解设计模式》读书笔记3-2 Prototype模式
    《图解设计模式》读书笔记3-1 Singleton模式
    《图解设计模式》读书笔记2-2 Factory Method模式
  • 原文地址:https://www.cnblogs.com/shanchuan/p/13382136.html
Copyright © 2020-2023  润新知