单元测试七:Mock2

一、mock介绍

在python3中,mock被集成到unittest模块中了。

1.什么是mock:

Mock 这个库的主要功能是模拟一些东西。

准确的说,Mock是Python中一个用于支持单元测试的库,它的主要功能是使用mock对象替代掉指定的Python对象,以达到模拟对象的行为。

2.mock示例:

假设你开发的项目叫a,里面包含了一个模块b,模块b中的一个函数c(也就是a.b.c)在工作的时候需要调用发送请求给特定的服务器来得到一个JSON返回值,然后根据这个返回值来做处理。

如果要为a.b.c函数写一个单元测试,该如何做? 
一个简单的办法是搭建一个测试的服务器,在单元测试的时候,让a.b.c函数和这个测试服务器交互。但是这种做法有两个问题:

测试服务器可能很不好搭建,或者搭建效率很低。 
你搭建的测试服务器可能无法返回所有可能的值,或者需要大量的工作才能达到这个目的。 
那么如何在没有测试服务器的情况下进行上面这种情况的单元测试呢?

Mock模块就是答案。上面已经说过了,mock模块可以替换Python对象。我们假设a.b.c的代码如下:

client.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests

def send_request(url):
    r = requests.get(url)
    return r.status_code

def visit_ustack():
    return send_request('http://www.ustack.com')

 client_test.py

import unittest
from unittest import mock
import client


class TestClient(unittest.TestCase):
    def test_success_request(self):
        success_send = mock.Mock()
        success_send.return_value = 200
        client.send_request = success_send
        self.assertEqual(client.visit_ustack(), 200)

    def test_failure_request(self):
        fail_send = mock.Mock()
        fail_send.return_value = 404
        client.send_request = fail_send
        self.assertEqual(client.visit_ustack(), 404)

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

3.Mock对象

Mock对象是mock模块中最重要的概念。Mock对象就是mock模块中的一个类的实例,这个类的实例可以用来替换其他的Python对象,来达到模拟的效果。Mock类的原型:

mock.Mock(spec=None, side_effect=None, return_value="DEFAULT",
          wraps=None, name=None, spec_set=None, parent=None,
          _spec_state=None ,_new_name="",_new_parent=None, **kwargs)

4.Mock对象的一般用法是这样的:

  • 找到你要替换的对象,这个对象可以是一个类,或者是一个类实例,或者是一个函数。
  • 然后实例化Mock类得到一个mock对象,并且设置这个mock对象的行为,比如被调用的时候返回什么值,被访问成员的时候返回什么值等。
  • 使用这个mock对象替换掉我们想替换的对象,也就是步骤1中确定的对象。
  • 之后就可以开始写测试代码,这个时候我们可以保证我们替换掉的对象在测试用例执行的过程中行为和我们预设的一样。

5.mock对象的自动创建

当访问一个mock对象中不存在的属性时,mock会自动建立一个子mock对象,并且把正在访问的属性指向它,这个功能对于实现多级属性的mock很方便。

二、mock用法

1.mock一个函数:直接使用mock.Mock()实例

可以在初始化的时候设定返回值myMethod = Mock(return_value=3),也可以通过myMethod.return_value的属性来设置。

from unittest.mock import Mock
myMethod = Mock()   # mock一个函数对象
myMethod.return_value = 3 # 设置返回值
print(myMethod(1, 'a', foo='bar'))  # 调用函数。返回值3

myMethod.assert_called_with(1, 'a', foo='bar')  # 断言此函数,在参数为(1, 'a', foo='bar')时被调用过。断言结果为True
print(myMethod())  # 调用函数。返回值3
print(myMethod.call_count)  # 此函数被调用过的次数:2

2.side_effect模拟一个异常或函数,此时参数是一个异常或函数

注意:当mock对象被调用时,side_effect函数同时被调用,而且是被mod对象的同样参数调用

示例:side_effect,为内部的一个标准异常函数。

此时,会抛出异常,因此应该将异常信息,同异常函数一样写出来。

myMethod.side_effect = KeyError("这是key错误的原因")
myMethod("abc")
# Traceback (most recent call last):
#....
KeyError: '这是key错误的原因'

new_mock = Mock(side_effect=KeyboardInterrupt("这是ctr+c中断错误的原因"))
new_mock()
# Traceback (most recent call last):
# ............
# KeyboardInterrupt: 这是ctr+c中断错误的原因

示例:side_effect,为自定义函数,可以用来动态地生成返回值。

此时,不一定抛出异常,应该接收mod对象的参数来使用,因此异常函数不能带参数。

除非,你想使用固定的写死的参数

def for_side(*args, **kwargs):
    print('args: ', args)
    print('kwargs: ', kwargs)
myMethod.side_effect = for_side
print(myMethod('dsf', **{"a": 1, "bn": 2}))
# args:  ('dsf',)
# kwargs:  {'a': 1, 'bn': 2}

 不能下面这样做:

myMethod.side_effect = for_side("abc")
print(myMethod('dsf', **{"a": 1, "bn": 2}))
# args:  ('abc',)
# kwargs:  {}

 示例:side_effect,为列表时,依次返回指定值;调用一次返回列表中的一个值

3.:side_effect动态的模拟一个参数列表,此时每调用一次Mock类的某个方法,就从参数列表中依次取一个参数,作为Mock类的某个方法的参数
import unittest
class Person:
    def __init__(self):
        self.__age = 10
    def get_fullname(self, first_name, last_name):
        return first_name + ' ' + last_name
    def get_age(self):
        return self.__age
    @staticmethod
    def get_class_name():
        return Person.__name__
class PersonTest(unittest.TestCase):
    def setUp(self):
        self.p = Person()
    def test_should_get_age(self):
        self.p.get_age = Mock(side_effect=[10, 11, 12])
        self.assertEqual(self.p.get_age(), 10)
        self.assertEqual(self.p.get_age(), 11)
        self.assertEqual(self.p.get_age(), 12)

    def test_should_get_fullname(self):
        values = {('James', 'Harden'): 'James Harden', ('Tracy', 'Grady'): 'Tracy Grady'}
        self.p.get_fullname = Mock(side_effect=lambda x, y: values[(x, y)])
        self.assertEqual(self.p.get_fullname('James', 'Harden'), 'James Harden')
        self.assertEqual(self.p.get_fullname('Tracy', 'Grady'), 'Tracy Grady')
if __name__ == '__main__':
    unittest.main()

4.对象构造器:mock一个类或函数:mock.create_autospec()

有时候需要模拟一个函数或者类的行为,包括它所有的属性和方法,如果手动去一个个添加,实在低效而且容易出错。mock提供了autospec的功能,根据提供的模板类生成一个mock实例。

下面是mock一个函数的例子:

def myFunc(a, b, c):
    pass
mock_func = mock.create_autospec(myFunc, return_value=3)

mock_func('aaaa', '2323', 2)
mock_func()
# TypeError: missing a required argument: 'a'

 mock一个类的示例:

class MyClass(object):
    def a(self):
        print('aaaaaaa') #nit是mock对象的构造器,name是mock对象的唯一标识;spec设置的是mock对象的属性,可以是property或者方法,也可以是其他的列表字符串或者其他的python类;return_value设置的是,当这个mock对象被调用的时候,显示出的结果就是return_value的值;side_effect是和return_value是相反的,覆盖了return_value,也就是说当这个mock对象被调用的时候,返回的是side_effect的值,而不是return_value。 
    def b(self):
        print('b  called')
    def c(self):
        print('c called')
mock_class = mock.create_autospec(MyClass)
mock_class.a()
mock_class().a()
mock_class.d()
# AttributeError: Mock object has no attribute 'd'

 以上方法:使用的是mock对象的构造器

参数:name是mock对象的唯一标识

spec设置的是mock对象的属性,可以是property或者方法,也可以是其他的列表字符串或者其他的python类;

return_value设置的是,当这个mock对象被调用的时候,显示出的结果就是return_value的值;

side_effect是和return_value是相反的,覆盖了return_value,也就是说当这个mock对象被调用的时候,返回的是side_effect的值,而不是return_value。 

5.mock.patch,mock.patch.object

module.py位于项目之下

class Person:
    def get_class_name(self):
        pass

 

module_test.py

from unittest import mock
import unittest
from module import Person

class PersonTest(unittest.TestCase):
    # 1. 在测试方法参数中得到Mock对象
    # 以字符串的形式列出静态方法的路径,在测试的参数里会自动得到一个Mock对象
    @mock.patch('module.Person.get_class_name')
    def test_should_get_class_name1(self, mock_get_class_name):
        mock_get_class_name.return_value = 'Guy'
        self.assertEqual(Person.get_class_name(), 'Guy')

    # 2. 在patch中设置Mock对象
    mock_get_class_name = mock.Mock(return_value='Guy')
    # 在patch中给出定义好的Mock的对象,好处是定义好的对象可以复用
    @mock.patch('module.Person.get_class_name', mock_get_class_name)
    def test_should_get_class_name2(self):
        self.assertEqual(Person.get_class_name(), 'Guy')

    # 3. 使用patch.object
    mock_get_class_name = mock.Mock(return_value='Guy')
    # 使用patch.object来mock,好处是Person类不是以字符串形式给出的
    @mock.patch.object(Person, 'get_class_name', mock_get_class_name)
    def test_should_get_class_name3(self, ):
        self.assertEqual(Person.get_class_name(), 'Guy')

    # 4. 使用with控制作用域
    # 作用域之外,依然返回真实值
    def test_should_get_class_name4(self, ):
        mock_get_class_name = mock.Mock(return_value='Guy')
        with mock.patch('module.Person.get_class_name', mock_get_class_name):
            self.assertEqual(Person.get_class_name(), 'Guy')
        self.assertEqual(Person().get_class_name(), 'Guy')

 

6.mock提供的断言语句:

assert_called_with(指定的参数) :    1.mock对象是否被调用过,任何时侯;2.上次调用是否以此处指定的参数为参数。

    当没调用过,或者参数错误时,断言失败。

    当至少有一个参数有错误的值或者类型时,当参数的个数出错时,当参数的顺序不正确时,这个断言就会被判断为失败。

示例:

class MyClass(object):
    def a(self):
        print('aaaaaaa')
    def b(self, num):
        print('b  called', num)
    def c(self):
        print('c called')
# mock_class = mock.create_autospec(MyClass)
# 或者
mock_class = mock.Mock(spec=MyClass)
mock_class.a()
# mock_class.assert_called_with()  # 断言将失败:因为这里断言的是mock_class是否被调用;而不是mock_class.a是否被调用
# AssertionError: Expected call: mock()
# Not called
mock_class.a.assert_called_with()  # 断言通过
mock_class.b(3)
mock_class.b.assert_called_with(5)  # 调用过,参数个数一致,参数类型一致,这些断言成功;但是参数值不一样。只要不是以此处指的参数调用的,断言都将失败。
# AssertionError: Expected call: b(5)
# Actual call: b(3)

 assert_called_once_with(指定的参数)   1.mock对象是否只被调用过一次;2.上次是否以(......)中的参数为参数调用的。

示例:

mock_class = mock.Mock(spec=MyClass)
mock_class.b(9)
mock_class.b.assert_called_once_with()
# 断言失败:只调用过一次,通过。但是调用的时侯是有参数的,而这里判断的是没有参数
# AssertionError: Expected call: b()
# Actual call: b(9)
mock_class = mock.Mock(spec=MyClass)
mock_class.b(9)
mock_class.b.assert_called_once_with(9)  # 断言通过
mock_class = mock.Mock(spec=MyClass)
mock_class.b(9)
mock_class.b.assert_called_once_with(9)  # 断言通过
mock_class.b(8)
mock_class.b.assert_called_once_with(8) # 断言失败
# 参数判断通过;但调用次数判断不通过
# AssertionError: Expected 'b' to be called once. Called 2 times.

 assert_any_call(指定的参数)   1.mock对象是否被调用过,任何时侯;2.调用是否以(......)中的参数为参数,任何时侯。

mock_class = mock.Mock(spec=MyClass)
mock_class.b(8)
mock_class.b.assert_any_call()  # 断言失败
# 参数判断不通过
# AssertionError: b() call not found
mock_class = mock.Mock(spec=MyClass)
mock_class.b()
mock_class.b(8)
mock_class.b.assert_any_call()  # 断言通过
mock_class.b(9)
mock_class.b.assert_any_call()  # 断言通过
# 任何时侯以此中指定的参数为参数调用过,即通过

 assert_has_calls(calls) :      mock对象,指定的参数被调用过

要指定一个参数calls,类型为列表:calls中,确定要调用的方法,作为参数

可以在调用后指定,也可以在调用前指定;只要在断言前指定了,即可

mock_class = mock.Mock(spec=MyClass)
from unittest.mock import call
mock_class.a()
mock_class.b(8)
mock_class.c()
calls = [call.a(), call.b(8), call.c()]
mock_class.assert_has_calls(calls)  # 断言通过
python3.6:assert_not_called()是否被调用过
    def test_should_validate_method_calling(self):
        get_fullname = Mock(return_value='James Harden')

        # 没调用过
        get_fullname.assert_not_called()  # Python 3.5

        get_fullname('1', '2')

        # # 调用过任意次数
        # get_fullname.assert_called()  # Python 3.6
        # # 只调用过一次, 不管参数
        # get_fullname.assert_called_once()  # Python 3.6
        # 只调用过一次,并且符合指定的参数
        get_fullname.assert_called_once_with('1', '2')

        get_fullname('3', '4')
        # 只要调用过即可,必须指定参数
        get_fullname.assert_any_call('1', '2')

        # 重置mock,重置之后相当于没有调用过
        get_fullname.reset_mock()
        get_fullname.assert_not_called()

        # Mock对象里除了return_value, side_effect属性外,
        # called表示是否调用过,call_count可以返回调用的次数
        self.assertEqual(get_fullname.called, False)
        self.assertEqual(get_fullname.call_count, 0)

6.将一个mock对象添加到另一个mock对象中:mock对象的attach_mock()方法

示例:将mockFoo2添加到mockFoo1中,并重新命名为mock_att。  

class Foo1(object):
    _value1 = 100
    def fun1(self):
        pass

class Foo2(object):
    _value2 = 100

    def fun50(self):
        pass
mockFoo1 = Mock(spec=Foo1)
mockFoo2 = Mock(spec=Foo2)
mockFoo1.attach_mock(mockFoo2, 'mock_att')
print(mockFoo1.mock_att._value2)
# <Mock name='mock.mock_att._value2' id='140208844551224'>

为什么mockFoo1._value2取到的不是200,而是一些id信息呢?

因为spec只是设置的是mock对象的属性,也就是说mock对象有这些属性,但是属性的值呢~~就说不定了!!! 

7.改变mock对象的属性:mock对象的configure_mock()方法

如,更改mock对象的return_value值:mock对象的configure_mock(return_value=new)方法

class Foo1(object):
    _value1 = 100
    def fun1(self):
        pass
mockFoo1 = Mock(spec=Foo1, return_value=123)
print(mockFoo1())  # 返回值:123
mockFoo1.configure_mock(return_value=555555)
print(mockFoo1())  # 返回值:555555

8.给mock对象添加一个新的属性:mock对象的mock_add_spec(aSpec, spec_set = False)方法

这个函数就是给mock对象添加一个新的属性(mock对象原来的属性就会被擦除),第二个参数是指属性是可读可写,默认是只读,如果想让其拥有写权限,可以将其设置为true.

class Foo(object):
    _value = 100
    def fun1(self):
        pass
def add_fn():
    pass
mockFoo = Mock(spec=Foo)
mockFoo.mock_add_spec(add_fn)  # 添加新的spec,原来的spec将抹除
# mockFoo.fun1()  # 原来的spec没有了,也没有了fun1属性
# AttributeError: Mock object has no attribute 'fun1'
# mockFoo.add_fn()  # 访问错误:因为新添加的属性,要和原来的属性对应;是类都是类
# AttributeError: Mock object has no attribute 'add_fn'

class Add(object):
    def fun5(self):
        pass
mockFoo.mock_add_spec(Add)
mockFoo.fun5() 

9.mock的统计:

1).called:跟踪mock对象所做的任意调用的访问器;

class Foo(object):
    _value = 100
    def fun1(self):
        pass
mockFoo = Mock(spec=Foo)
mockFoo.fun1()
print(mockFoo.called)  # False
mockFoo()
print(mockFoo.called)  # True

2).call_count:mock对象被工厂调用的次数:

class Foo(object):
    _value = 100
    def fun1(self):
        pass
mockFoo = Mock(spec=Foo)
mockFoo.fun1()
print(mockFoo.call_count)  # 0
mockFoo()
print(mockFoo.call_count)  # 1

3).call_args:获取工厂调用时的参数(最近使用的参数):

mockFoo = Mock(spec=Foo)
mockFoo.fun1()
print(mockFoo.call_args)  # None
mockFoo('a', 'b')
print(mockFoo.call_args)  # call('a', 'b')

4).call_args_list:获取工厂调用的的所有参数,是个列表;

class Foo(object):
    _value = 100
    def fun1(self):
        pass
mockFoo = Mock(spec=Foo)
mockFoo.fun1()
print(mockFoo.call_args_list)  # []
mockFoo('a', 'b')
mockFoo('c')
mockFoo()
print(mockFoo.call_args_list)  # [call('a', 'b'), call('c'), call()]

5).method_calls:测试一个mock对象都调用了哪些方法,结果是一个list

class Foo(object):
    _value = 100
    def fun1(self):
        pass
mockFoo = Mock(spec=Foo)
mockFoo.fun1()
print(mockFoo.method_calls)  # [call.fun1()]
mockFoo('a', 'b')
print(mockFoo.method_calls)  # [call.fun1()]
mockFoo.fun1()
print(mockFoo.method_calls)  # [call.fun1(), call.fun1()]
mockFoo._value
print(mockFoo.method_calls)  # [call.fun1(), call.fun1()]

10.mock链式调用,访问数据库

在django里,我们经常需要mock数据库,而访问数据库时经常是链式调用,看个例子。

def get_person(name):
    return Person.objects.filter(name=name).order_by('age')

有个模块方法,返回数据库中所有指定name的人员,并按age排序

mock掉整个数据库访问

@patch('your.package.module.Person.objects.filter')
def test_should_get_person(self, mock_filter):
    # 先得到一个filter的Mock对象,再在return_value中设置一个Mock对象,此时不需要自己再创建
    mock_filter.return_value.order_by.return_value = None
    
    self.assertIsNone(get_person())

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted on 2018-08-27 14:24  myworldworld  阅读(348)  评论(0)    收藏  举报

导航