Why use mock

mock,库如其名,是一个用来伪造对象的库 (🙃)。

想象一个这样的场景:

你开发了一个比较复杂的程序,它每次都需要读取外部一个数据库中的数据,后续的操作都会根据这个读取的数据来进行。

面对这样的程序,你会如何写测试的代码?

是将依赖的数据库信息在测试代码中也定义一份?还是伪造一份数据库中的数据,然后hack掉读数据的部分,直接用伪造的数据用于后续操作的测试?

OK,以上两种方式都是可以达到测试的效果的。BUT,前者的测试代码会依赖外部的数据库,如果数据库服务器发生变化,那么测试代码也要跟着变;而且如果涉及数据库的写入,为了不影响开发环境的数据库,可能需要单独搭建一个测试用的数据库;即使搭建了测试用的数据库,如果开发用的数据库配置发生变化,测试这边也得跟着变,想想就好麻烦。而后者使用Hack的方式来取代掉外部的数据库虽然可行,但测试代码会变得难以维护。

解决这个问题还有一个出路,那就是用mock库中的MagicMock对象来模拟和伪造一个数据库对象,它帮你干了你要干的hack的活,用这种方式要比直接hack源代码要更易于维护。

Play with mock

MagicMock

简单提几点:

  • 访问MagicMock类型的对象中未定义的属性会创建一个新MagicMock并绑定到这个属性。
  • MagicMock类型有一些特殊的属性:
    • 通过设定MagicMock类型对象的return_value属性,会把这个属性的值绑定为__call__方法的返回值。
    • 通过设定MagicMock类型对象的side_effect属性,可以达到使__call__方法根据输入来返回不同值的效果。

其他关于MagicMock的一些特性看下面的例子好了:

from mock import MagicMock

if __name__ == '__main__':
    # MagicMock object will generate a new MagicMock object if you visit to a undefined attribute.
    m0 = MagicMock()
    print m0.aaa

    # You can define some attributes in the construct function of MagicMock conveniently.
    m1 = MagicMock(aaa=1, bbb=2)
    print m1.aaa, m1.bbb
    # By `del` method, you can delete an attribute of MagicMock object, and it will cause AttributeError when you visit the deleted attribute.
    del m1.ccc
    try:
        print m1.ccc
    except AttributeError, e:
        print 'AttributeError'

    # Use spec to limit a set of attributes, will cause AttributeError when you visit attribute out of the spec list.
    m2 = MagicMock(spec=['aaa', 'bbb'])
    print m2.aaa
    try:
        print m2.ccc
    except AttributeError, e:
        print 'AttributeError'

    # Use spec to input a object can make the MagicMock object pass `isinstance` test.
    dd = dict()
    m3 = MagicMock(spec=dd)
    print isinstance(m3, dict)

    # Set the `return_value` to a MagicMock object will let the calling of the object always return the same value.
    m4 = MagicMock()
    m4.return_value = 6
    print m4(4, 'aaa'), m4([1,2,3])

    # Use side_effect to bind a method to the MagicMock object so that it can return different values according to the inputs.
    m4 = MagicMock(side_effect=lambda value: value * 2)
    print m4(3), m4('abc')

    # Can also bind a iterable object to side_effect. In this case, the return value will be the traversal of the iterable object.
    # And has nothing to do with the inputs.
    m5 = MagicMock()
    m5.side_effect = [5, 'hi']
    print m5(2), m5()

patch

通过patch可以方便地替换掉目标位置的对象,从而达到mock的目的。mock很贴心地帮patch写了装饰器和with语句的实现,让替换对象的scope更加一目了然。

使用patch要特别注意要替代的对象定位不能错(参见Mock patching for ‘from/import’ statement in Python),其他直接看下面的例子就行:

from mock import patch


def foo():
    return 'foo'


# patch can simply used by only inputting the target object that need to replaced.
# In this situation, you must append an argument to the method's input. This argument is a MagicMock object generated by patch decorator.
# So you can change the return value of the MagicMock object which will be the return value of calling the replaced object.
@patch('__main__.foo')
def test_one(mock_fuc):
    mock_fuc.return_value = 'bar'
    print foo
    print foo()
    # One goodness of inputting MagicMock object into method is here: you can make some assert of the mock object.
    mock_fuc.assert_called_once_with()
    print mock_fuc.call_count


def bar():
    return 'bar'


# patch can also used by inputting the replaced object in the decorator.
# In this situation, you do not need to append an argument to the method's input.
@patch('__main__.foo', bar)
def test_two():
    print foo
    print foo()


# Besides decorator, you can also use patch as `with` statement.
def test_three():
    with patch('__main__.foo', bar):
        print foo
        print foo()
    print foo
    print foo()


# Besides `with` statement, you can also control the scope by patch.start() and patch.stop() methods.
# This is more flexible than the methods above, and can be useful when patch with several test cases.
def test_four():
    patcher = patch('__main__.foo', bar)
    patcher.start()
    print foo
    print foo()
    patcher.stop()
    print foo
    print foo()


# Can use several patch at the same time.
# When inputting the MagicMock objects to the method, notice the sequence of the MagicMock objects is reverse to the decorators.
@patch('__main__.foo')
@patch('__main__.bar')
def test_five(mock_bar, mock_foo):
    mock_foo.return_value = 'zzz'
    mock_bar.return_value = 'mew'
    print foo
    print foo()
    print bar
    print bar()


if __name__ == '__main__':
    test_one()
    test_two()
    test_three()
    test_four()
    test_five()

patch.object, patch.dict, patch.multiply

这三个东西其实就是让你少些了几行代码,当然用还是蛮好用的:

from mock import patch, DEFAULT


class Real(object):
    def foo(self):
        return 'foo'

    def bar(self):
        return 'bar'


# Use `patch.object` to replace only one attribute of the object.
# (This function can be also implemented by `patch`, but this is more convenient)
@patch.object(Real, 'foo')
def test_one(mock_foo):
    mock_foo.return_value = 'zzz'
    foo = Real()
    print foo.foo()
    print foo.bar()


real_dict = {'foo': 1, 'bar': 2, 'zzz': 3}


# Use `patch.dict` to 'revise' a dict like object conveniently.
@patch.dict(real_dict, foo=2, bar=3)
def test_two():
    print real_dict


# Use `patch.multiple` to replace several objects in the same module.
@patch.multiple('__main__', real_dict={}, Real=DEFAULT)
def test_three(Real):
    print real_dict
    print Real


if __name__ == '__main__':
    test_one()
    test_two()
    test_three()

patch.TEST_PREFIX

因为mock这个库本身是为测试而写出来的,在Python 3中也是作为unittest库的一部分存在的,所以它默认只会替换测试方法中的对象,而ignore非测试方法。现在主流的测试框架对测试方法的判断就是看它是不是以test为开头命名的,所以mock也是这么判断的。

如果用mock不是用于测试,可以修改patch.TEST_PREFIX的值来让它生效:

from mock import patch

# By default, the patchers recognise methods that start with 'test' as being test methods.
# We set the prefix to `foo` here so that the patch will influence all the methods whose name start with `foo`
patch.TEST_PREFIX = 'foo'
value = 3


@patch('__main__.value', 'not three')
class Thing(object):
    def foo_one(self):
        print value

    def foo_two(self):
        print value

    def bar_one(self):
        print value


if __name__ == '__main__':
    t = Thing()
    t.foo_one()
    t.foo_two()
    t.bar_one()

An example

最后看下官方文档的一个例子,我添加了一些注释:

from mock import patch


class Foo(object):
    def print_foo(self):
        return 'foo'


def some_function():
    instance = Foo()
    print instance
    return instance.print_foo()


if __name__ == '__main__':
    # Will create a MagicMock object to replace class `Foo`
    with patch("__main__.Foo") as MockFoo:
        # Call the MagicMock object will return a new MagicMock object and is bounded with the calling MockFoo()
        # (means each time you call MockFoo()/Foo(), will return the same MagicMock object)
        mock_instance = MockFoo.return_value
        # Set the print_foo()'s return value to 'bar'
        mock_instance.print_foo.return_value = 'bar'
        print mock_instance
        # Call the function which calls Foo.print_foo(), this actually calls the mock_instance.print_foo()
        result = some_function()
        assert result == 'bar'