【全面解析Mock】Mock在单元测试中扮演一个什么角色?

目录

一、Mock在单元测试中扮演一个什么角色

二、测试准备

三、使用Mock的理由

四、使用Python Mock

五、MagicMock类

六、mock.create_autospce

七、mock.patch和mock.patch.object

八、python3中的mock(前面介绍的是python2的)

1、简单的例子

2、完成功能测试

3、解决测试依赖


一、Mock在单元测试中扮演一个什么角色

  有时,你需要为单元测试的初始设置准备一些“其他”的代码资源。但这些资源兴许会不可用,不稳定,或者是使用起来太笨重。你可以试着找一些其他的资源替代;或者你可以通过创建一个被称为mock的东西来模拟它。Mocks能够让我们模拟那些在单元测试中不可用或太笨重的资源。
  
   在Python中创建mock是通过Mock模块完成的。你可以通过每次一个属性(one-attribute-at-a-time)或一个健全的字典对象或是一个类接口来创建mock。你还可以定义mock的行为并且在测试过程中检查它的使用。让我们继续探讨。

二、测试准备

Test Case Test Subject Test Resource

  典型的测试准备最少有两个部分。首先是测试对象(红色),这是测试的关注点。它可以是一个方法、模块或者类。它可以返回一个结果,也可以不返回结果,但是它可以根据数据数据或者内部状态产生错误或者异常。
  
  第二测试用例(灰色),它可以单独运行也可以作为套件的一部分。它是为测试对象准备的,也可以是测试对象需要的任意数据或资源。运行一个或多个测试事务,在每个测试中检查测试对象的行为。收集测试结果并用一个简洁、易读的格式呈现测试结果。
  
  现在,为了发挥作用,一些测试对象需要一个或多个资源(绿色)。这些资源可以是其他的类或者模块,甚至是一个非独立的进程。不论其性质,测试资源是功能性的代码。他们的角色是支持测试对象,但是他们不是测试的关注点。

三、使用Mock的理由

  但是有些时候,测试资源不可用,或者不适合。也许这个资源正在和测试对象并行开发中,或者并不完整或者是太不稳定以至于不可靠。
  
  测试资源太昂贵,如果测试资源是第三方的产品,其高昂的价格不适用于测试。测试资源的建立过于复杂,占用的硬件和时间可以用于别的地方。如果测试资源是一个数据源,建立它的数据集模仿真实世界是乏味的。
  
  测试资源是不可预知的。一个好的单元测试是可重复的,允许你分离和识别故障。但是测试资源可能给出随机的结果,或者它会有不同的响应时间。而作为这样的结果,测试资源最终可能成为一个潜在的搅局者。
  
   这些都是你可能想要用mock代替测试资源的原因。mock向测试对象提供一套和测试资源相同的方法接口。但是mock是更容易创建和管理。它能向测试对象提供和真实的测试资源相同的方法接口。它能提供确定的结果,并可以自定义以适用于特定的测试。能够容易的更新,以反映实际资源的变化。
  
  当然,mocks不是没有问题的。设计一个精确的mock是困难的,特别是如果你没有测试资源的可靠信息。你可以尝试找到一个开源的接口,或者你能对测试资源的方法接口进行猜测。无论你如何选择,你都可以在以后轻松的更新mock,你可以在首选资源中得到更详细的信息。
  
  太多的mock会使测试过于复杂,让你跟踪错误变得更困难。最好的实践是每个测试用例限制使用一到两个mock,或者为每个mock/对象对使用独立的测试用例。

四、使用Python Mock

  在Python中Mock模块是用来创建和管理mock对象的。该模块是Michael Foord的心血结晶,它是Python3.0的标准模块。因此在Python2.4~2.7中,你不得不自己安装这个模块。你可以 Python Package Index website从获得Mock模块最新的版本。
  Mock模块中有两个非常重要的类Mock、MagicMock和一个重要的方法create_autospec。

五、MagicMock类

MagicMock类是Mock类的子类,区别在于MagicMock类实现了常用的魔术方法,比如__str__、__iter__等,其他一样。

六、mock.create_autospce

  mock.create_autospec为类提供了一个同等功能实例。这意味着,实际上来说,在使用返回的实例进行交互的时候,如果使用了非法的方法将会引发异常。更具体地说,如果一个方法被调用时的参数数目不正确,将引发一个异常。这对于重构来说是非常重要。当一个库发生变化的时候,中断测试正是所期望的。如果不使用auto-spec,即使底层的实现已经破坏,我们的测试仍然会通过。
  在选择使用mock.Mock实例,mock.MagicMock实例或create_autospec方法的时候,通常倾向于选择使用 create_autospec方法,因为它能够对未来的变化保持测试的合理性。这是因为mock.Mock和mock.MagicMock会无视底层的API,接受所有的方法调用和参数赋值。

class Target(object):
    def apply(value):
        return valuedef method(target, value)
    return target.apply(value)

我们像下面这样使用mock.Mock实例来做测试:

class MethodTestCase(unittest.TestCase):
    def test_method(self):
        target = mock.Mock()
        method(target, 'value')
        target.apply.assert_called_with('value')

这个逻辑看似合理,但如果我们修改Target.apply方法接受更多参数:

class Target(object):
    def apply(value, are_you_sure):
        if are_you_sure:
            return value
        else:
            return None

重新运行你的测试,然后你会发现它仍然能够通过。这是因为它不是针对你的API创建的。这就是为什么你总是应该使用create_autospec方法,并且在使用@patch和@patch.object装饰方法时使用autospec参数。

七、mock.patch和mock.patch.object

1.参数

unittest.mock.patch(target,new = DEFAULT,spec = None,create = False,spec_set = None,autospec = None,new_callable = None,** kwargs )

    • target参数必须是一个str,格式为'package.module.ClassName',
      注意这里的格式一定要写对,如果你的函数或类写在pakege名称为a下,b.py脚本里,有个c的函数(或类),那这个参数就写“a.b.c”
    • new参数如果没写,默认指定的是MagicMock
    • spec=True或spec_set=True,这会导致patch传递给被模拟为spec / spec_set的对象
    • new_callable允许您指定将被调用以创建新对象的不同类或可调用对象。默认情况下MagicMock使用。

注意:@mock.patch整个对象,@mock.patch.object对象中的方法

示例

# 文件名:Mymodel
class MyTest(object):
    def func(self):
        pass

那么@mock.patch

import Mymodel


class AppMockTests(unit.TestCase):

    def setUp(self):
        super(AppMockTests, self).setUp()
        self.project_zbj = webtest.TestApp(self.loadapp('project_zbj'))

    @mock.patch('Mymodel.MyTest')  # 必须是字符串,具体到类
    def test_list(self, mock_MyTest):
        url = '/api/zzz'
        mock_MyTest.func.return_value = 500
        res = self.project_zbj.get(url)
        self.assertEqual(res.json['data'], 500)

而@mock.patch.object

import Mymodel


class AppMockTests(unit.TestCase):

    def setUp(self):
        super(AppMockTests, self).setUp()
        self.project_zbj = webtest.TestApp(self.loadapp('project_zbj'))

    @mock.patch.object(Mymodel.MyTest, 'func')  # 具体到某个方法 
    def test_list(self, mock_func):
        url = '/api/zzz'
        mock_func.return_value = 500
        res = self.project_zbj.get(url)
        self.assertEqual(res.json['data'], 500)

八、python3中的mock(前面介绍的是python2的)

  Mocks让我们为单元测试模拟了那些不可用或者是太庞大的资源。我们可以在运行中配置mock,在特定的测试中改变它的行为或响应,或者让它在恰当的时候抛出错误和异常。

 但在,实际生产中的项目是非常复杂的,对其进行单元测试的时候,会遇到以下问题:

  • 接口的依赖
  • 外部接口调用
  • 测试环境非常复杂

  单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的.使用mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而使得单元测试将焦点只放在当前的单元功能。

1、简单的例子

我们先从最简单例子开始。

modular.py

#modular.py

class Count():

    def add(self):
        pass

这里要实现一个Count计算类,add() 方法要实现两数相加。但,这个功能我还没有完成。这时就可以借助mock对其进行测试。

mock_demo01.py

from unittest import mock
import unittest

from modular import Count

# test Count class
class TestCount(unittest.TestCase):

    def test_add(self):
        count = Count()
        count.add = mock.Mock(return_value=7)
        result = count.add(2,5)
        self.assertEqual(result,7)
        # 如果是python2,那么return_value的值会保存在返回值的json格式中的data
     self.assertEqual(result.json['data'], 7)


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

  count = Count()

  首先,调用被测试类Count() 。

  count.add = mock.Mock(return_value=7)

  通过Mock类模拟被调用的方法add()方法,return_value 定义add()方法的返回值。

  result = count.add(2,5)

  接下来,相当于在正常的调用add()方法,传两个参数2和5,然后会得到相加的结果7。然后,7的结果是我们在上一步就预先设定好的。

  self.assertEqual(result,7)

  最后,通过assertEqual()方法断言,返回的结果是否是预期的结果7。

  运行测试结果:

> python3 mock_demo01.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

这样一个用例就在mock的帮助下编写完成,并且测试通过了。

2、完成功能测试

再接下来完成module.py文件中add()方法。

# module.py

class Count():

    def add(self, a, b):
        return a + b

然后,修改测试用例:

from unittest import mock
import unittest
from module import Count


class MockDemo(unittest.TestCase):

    def test_add(self):
        count = Count()
        count.add = mock.Mock(return_value=13, side_effect=count.add)
        result = count.add(8, 8)
        print(result)
        count.add.assert_called_with(8, 8)
        self.assertEqual(result, 16)

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

 count.add = mock.Mock(return_value=13, side_effect=count.add)

  side_effect参数和return_value是相反的。它给mock分配了可替换的结果,覆盖了return_value。简单的说,一个模拟工厂调用将返回side_effect值,而不是return_value。

  所以,设置side_effect参数为Count类add()方法,那么return_value的作用失效。

  result = count.add(8, 8)

  print(result)

  这次将会真正的调用add()方法,得到的返回值为16(8+8)。通过print打印结果。

  assert_called_with(8,8)

  检查mock方法是否获得了正确的参数。

3、解决测试依赖

  前面的例子,只为了让大家对mock有个初步的印象。再接来,我们看看如何mock方法的依赖。

  例如,我们要测试A模块,然后A模块依赖于B模块的调用。但是,由于B模块的改变,导致了A模块返回结果的改变,从而使A模块的测试用例失败。其实,对于A模块,以及A模块的用例来说,并没有变化,不应该失败才对。

  这个时候就是mock发挥作用的时候了。通过mock模拟掉影响A模块的部分(B模块)。至于mock掉的部分(B模块)应该由其它用例来测试。

# function.py
def add_and_multiply(x, y):
    addition = x + y
    multiple = multiply(x, y)
    return (addition, multiple)


def multiply(x, y):
    return x * y

  然后,针对 add_and_multiply()函数编写测试用例。func_test.py

import unittest
import function


class MyTestCase(unittest.TestCase):

    def test_add_and_multiply(self):
        x = 3
        y = 5
        addition, multiple = function.add_and_multiply(x, y)
        self.assertEqual(8, addition)
        self.assertEqual(15, multiple)


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

运行结果:

>  python3 func_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

  目前运行一切正确常,然而,add_and_multiply()函数依赖了multiply()函数的返回值。如果这个时候修改multiply()函数的代码。

……
def multiply(x, y):
    return x * y + 3

  这个时候,multiply()函数返回的结果变成了x*y加3。

  再次运行测试:

>  python3 func_test.py                                                   
F                                                                       
======================================================================  
FAIL: test_add_and_multiply (__main__.MyTestCase)                       
----------------------------------------------------------------------  
Traceback (most recent call last):                                      
  File "fun_test.py", line 19, in test_add_and_multiply                 
    self.assertEqual(15, multiple)                                      
AssertionError: 15 != 18                                                
                                                                        
----------------------------------------------------------------------  
Ran 1 test in 0.000s                                                    
                                                                        
FAILED (failures=1)   

  测试用例运行失败了,然而,add_and_multiply()函数以及它的测试用例并没有做任何修改,罪魁祸首是multiply()函数引起的,我们应该把 multiply()函数mock掉。

import unittest
from unittest.mock import patch
import function


class MyTestCase(unittest.TestCase):

    @patch("function.multiply")
    def test_add_and_multiply2(self, mock_multiply):
        x = 3
        y = 5
        mock_multiply.return_value = 15
        addition, multiple = function.add_and_multiply(x, y)
        mock_multiply.assert_called_once_with(3, 5)

        self.assertEqual(8, addition)
        self.assertEqual(15, multiple)


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

  @patch("function.multiply")

  patch()装饰/上下文管理器可以很容易地模拟类或对象在模块测试。在测试过程中,您指定的对象将被替换为一个模拟(或其他对象),并在测试结束时还原。

  这里模拟function.py文件中multiply()函数。

  def test_add_and_multiply2(self, mock_multiply):

  在定义测试用例中,将mock的multiply()函数(对象)重命名为 mock_multiply对象

  mock_multiply.return_value = 15

  设定mock_multiply对象的返回值为固定的15。

  ock_multiply.assert_called_once_with(3, 5)

  检查ock_multiply方法的参数是否正确。

  再次,运行测试用例,通过!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值