Python 的mock模拟测试介绍

如何不靠耐心测试

可能咱们正在写一个社交软件而且想测试一下“发布到Facebook的功能”,可是咱们不但愿每次运行测试集的时候都发布到Facebook上。

Python的unittest库中有一个子包叫unittest.mock——或者你把它声明成一个依赖,简化为mock——这个模块提供了很是强大而且有用的方法,经过它们能够模拟或者屏敝掉这些不受咱们但愿的方面。html

Python 的模拟测试介绍

注意:mock是最近收录在Python 3.3标准库中的;以前发布的版本必须经过 PyPI下载Mock库。python

恐惧系统调用

不管你是想写一个脚本弹出一个CD驱动,或者是一个web服务用来删除/tmp目录下的缓存文件,或者是一个socket服务来绑定一个TCP端口,这些调用都是在你单元测试的时候是不被但愿的方面。web

做为一个开发人员,你更关心你的库是否是成功的调用了系统函数来弹出CD,而不是体验每次测试的时候CD托盘都打开。缓存

对于咱们的第一个例子,咱们要重构一个从原始到使用mock的一个标准Python测试用例。咱们将会证实如何用mock写一个测试用例使咱们的测试更智能、更快,而且能暴露更多关于咱们的软件工做的问题。app

一个简单的删除功能

有时,咱们须要从文件系统中删除文件,所以,咱们能够写这样的一个函数在Python中,这个函数将使它更容易成为咱们的脚本去完成这件事情。socket

#!/usr/bin/env python
-*- coding: utf-8 -*-
import os
def rm(filename):
    os.remove(filename)

让咱们写一个传统的测试用例,即,不用模拟测试:ide

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import rm
import os.path
import tempfile
import unittest
class RmTestCase(unittest.TestCase):

    tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile")    
	
	def setUp(self):
        with open(self.tmpfilepath, "wb") as f:
            f.write("Delete me!")        
    def test_rm(self):
        # remove the file
        rm(self.tmpfilepath)        # test that it was actually removed
        self.assertFalse(os.path.isfile(self.tempfile), "Failed to remove the file.")

当它每次运行时,一个临时文件被建立而后被删除。咱们没有办法去测试咱们的rm方法是否传递参数到os.remove中。咱们能够假设它是基于上面的测试,但仍有许多须要被证明。函数

重构与模拟测试

让咱们使用mock重构咱们的测试用例:post

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import rm
import mock
import unittest

class RmTestCase(unittest.TestCase):
     
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os):
        rm("any path")        # test that rm called os.remove with the right parameters
        mock_os.remove.assert_called_with("any path")

对于这些重构,咱们已经从根本上改变了该测试的运行方式。单元测试

如今,mymodule模块中的os对象已经被mock对象替换,当调用mymodule的os模块的remove方法时,实际调用的是mock_os这个mock对象的remove方法。

向‘rm’中加入验证

以前定义的 rm 方法至关的简单 . 在盲目的删除以前,咱们会拿它来验证一个路径是否存在,验证其是不是一个文件. 让咱们重构 rm :

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
import os
import os.path

def rm(filename):
    if os.path.isfile(filename):
        os.remove(filename)

如今,让咱们调整咱们的测试用例来保持测试的覆盖程度.

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import rm
import mock
import unittest

class RmTestCase(unittest.TestCase):
     
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # set up the mock
        mock_path.isfile.return_value = False
         
        rm("any path")        
        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")        
        # make the file 'exist'
        mock_path.isfile.return_value = True
         
        rm("any path")
         
        mock_os.remove.assert_called_with("any path")

咱们的测试范例彻底变化了.mymodule的os模块的isfile方法也被mock对象替换。

将删除功能做为服务

到目前为止,咱们只是对函数功能提供模拟测试,并没对须要传递参数的对象和实例的方法进行模拟测试。接下来咱们将介绍如何对对象的方法进行模拟测试。

首先,咱们先将rm方法重构成一个服务类。下面是重构的代码:

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
import os
import os.path

class RemovalService(object):
    """A service for removing objects from the filesystem."""

    def rm(filename):
        if os.path.isfile(filename):
            os.remove(filename)

你能够发现咱们的测试用例实际上没有作太多的改变:

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import RemovalService
import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):
     
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()        
        # set up the mock
        mock_path.isfile.return_value = False
         
        reference.rm("any path")        
        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")        
        # make the file 'exist'
        mock_path.isfile.return_value = True
         
        reference.rm("any path")
         
        mock_os.remove.assert_called_with("any path")

很好,RemovalService如同咱们计划的同样工做。接下来让咱们建立另外一个以该对象为依赖项的服务:

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
import os
import os.path

class RemovalService(object):
    """A service for removing objects from the filesystem."""

    def rm(filename):
        if os.path.isfile(filename):
            os.remove(filename)            

class UploadService(object):

    def __init__(self, removal_service):
        self.removal_service = removal_service       
		
    def upload_complete(filename):
        self.removal_service.rm(filename)

到目前为止,咱们的测试已经覆盖了RemovalService, 咱们不会对咱们测试用例中UploadService的内部函数rm进行验证。相反,咱们将调用UploadService的RemovalService.rm方法来进行简单的测试(为了避免产生其余反作用),咱们经过以前的测试用例能够知道它能够正确地工做。

有两种方法能够实现以上需求:

  1. 模拟RemovalService.rm方法自己。

  2. 在UploadService类的构造函数中提供一个模拟实例。

由于这两种方法都是单元测试中很是重要的方法,因此咱们将同时对这两种方法进行回顾。

选项1: 模拟实例的方法

该模拟库有一个特殊的方法用来装饰模拟对象实例的方法和参数。@mock.patch.object 进行装饰:

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import RemovalService, UploadService
import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):
     
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()        
        # set up the mock
        mock_path.isfile.return_value = False
         
        reference.rm("any path")        
        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")        
        # make the file 'exist'
        mock_path.isfile.return_value = True
         
        reference.rm("any path")
         
        mock_os.remove.assert_called_with("any path")      
       
class UploadServiceTestCase(unittest.TestCase):

    @mock.patch.object(RemovalService, 'rm')
    def test_upload_complete(self, mock_rm):
        # build our dependencies
        removal_service = RemovalService()
        reference = UploadService(removal_service)        
        # call upload_complete, which should, in turn, call `rm`:
        reference.upload_complete("my uploaded file")        
        # check that it called the rm method of any RemovalService
        mock_rm.assert_called_with("my uploaded file")        
        # check that it called the rm method of _our_ removal_service
        removal_service.rm.assert_called_with("my uploaded file")
这种修补机制实际上取代了咱们的测试方法的删除服务实例的rm方法。这意味着,咱们实际上能够检查该实例自己。若是你想了解更多,能够试着在模拟测试的代码中下断点来更好的认识这种修补机制是如何工做的。

@mock.patch.object用来对一个对象的某个方法或者属性进行替换。

陷阱:装饰的顺序

当使用多个装饰方法来装饰测试方法的时候,装饰的顺序很重要,但很容易混乱。基本上,当装饰方法呗映射到带参数的测试方法中时,装饰方法的工做顺序是反向的。好比下面这个例子:

@mock.patch('mymodule.sys')
@mock.patch('mymodule.os')
@mock.patch('mymodule.os.path')
def test_something(self, mock_os_path, mock_os, mock_sys):
    pass

注意到了吗,咱们的装饰方法的参数是反向匹配的? 这是有部分缘由是由于Python的工做方式。下面是使用多个装饰方法的时候,实际的代码执行顺序:

patch_sys(patch_os(patch_os_path(test_something)))

因为这个关于sys的补丁在最外层,所以会在最后被执行,使得它成为实际测试方法的最后一个参数。请特别注意这一点,而且在作测试使用调试器来保证正确的参数按照正确的顺序被注入。

选项2: 建立模拟测试接口

咱们能够在UploadService的构造函数中提供一个模拟测试实例,而不是模拟建立具体的模拟测试方法。 我推荐使用选项1的方法,由于它更精确,但在多数状况下,选项2是必要的而且更加有效。让咱们再次重构咱们的测试实例:

#!/usr/bin/env python# 
-*- coding: utf-8 -*-
from mymodule import RemovalService, UploadService
import mock
import unittest

class RemovalServiceTestCase(unittest.TestCase):
     
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    def test_rm(self, mock_os, mock_path):
        # instantiate our service
        reference = RemovalService()        
        # set up the mock
        mock_path.isfile.return_value = False
         
        reference.rm("any path")        
        # test that the remove call was NOT called.
        self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")        
        # make the file 'exist'
        mock_path.isfile.return_value = True
         
        reference.rm("any path")
         
        mock_os.remove.assert_called_with("any path")      
       
class UploadServiceTestCase(unittest.TestCase):

    def test_upload_complete(self, mock_rm):
        # build our dependencies
        mock_removal_service = mock.create_autospec(RemovalService)
        reference = UploadService(mock_removal_service)        
        # call upload_complete, which should, in turn, call `rm`:
        reference.upload_complete("my uploaded file")        
        # test that it called the rm method
        mock_removal_service.rm.assert_called_with("my uploaded file")

在这个例子中,咱们甚至不须要补充任何功能,只需建立一个带auto-spec方法的RemovalService类,而后将该实例注入到UploadService中对方法验证。

mock.create_autospec为类提供了一个同等功能实例。这意味着,实际上来讲,在使用返回的实例进行交互的时候,若是使用了非法的方法将会引起异常。更具体地说,若是一个方法被调用时的参数数目不正确,将引起一个异常。这对于重构来讲是很是重要。当一个库发生变化的时候,中断测试正是所指望的。若是不使用auto-spec,即便底层的实现已经破坏,咱们的测试仍然会经过。

陷阱:mock.Mock和mock.MagicMock类

mock库包含两个重要的类mock.Mockmock.MagicMock,大多数内部函数都是创建在这两个类之上的。在选择使用mock.Mock实例,mock.MagicMock实例或auto-spec方法的时候,一般倾向于选择使用 auto-spec方法,由于它可以对将来的变化保持测试的合理性。这是由于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参数。

真实世界的例子: 模仿一次 Facebook API 调用

在结束之际,让我写一个更加实用的真实世界的例子, 这在咱们的介绍部分曾今提到过: 向Facebook发送一个消息. 咱们会写一个漂亮的封装类,和一个产生回应的测试用例.

import facebook

class SimpleFacebook(object):
     
    def __init__(self, oauth_token):
        self.graph = facebook.GraphAPI(oauth_token)

	def post_message(self, message):
        """Posts a message to the Facebook wall."""
		self.graph.put_object("me", "feed", message=message)

下面是咱们的测试用例, 它检查到我发送了信息,但并无实际的发送出这条信息(到Facebook上):

import facebook
import simple_facebook
import mock
import unittest

class SimpleFacebookTestCase(unittest.TestCase):
     
    @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True)
    def test_post_message(self, mock_put_object):
        sf = simple_facebook.SimpleFacebook("fake oauth token")
        sf.post_message("Hello World!")        # verify
        mock_put_object.assert_called_with(message="Hello World!")

就咱们目前所看到的,在Python中用 mock 开始编写更加聪明的测试是真的很简单的.

如何用mock模拟python的builtin内建函数

from mymodule import test

class TestLogParse(unittest2.TestCase):

    @patch('__builtin__.open')
    def test_parse1(self,mock_open):
        mock_open.return_value = 'local'
        print open('abf')

    @patch('mymodule.open',create=True)
    def test_parse2(self,mock_open):
        mock_open.return_value = 'remote'
        test()

mock中side_effect的使用

为mock对象指定side_effect属性后,每次mock被调用,side_effect都将被调用,而且调用的参数也会被传递进来。咱们能够根据这点来作一些判断。

    @patch('mymodule.open',create=True)
    def test_parse(self,mock_open):
        def open_side_effect(*args, **kwargs):
            if len(args) == 1:
                return read_file
            else:
                return write_file
        read_file = StringIO.StringIO()
        write_file = StringIO.StringIO()
这里,根据open传递的参数个数来判断返回的对象。

总结

Python的 mock 库, 使用起来是有点子迷惑, 是单元测试的游戏规则变革者. 咱们经过开始在单元测试中使用 mock ,展现了一些一般的使用场景, 但愿这篇文章能帮助 Python 克服一开始的障碍,写出优秀的,能经得起测试的代码.

相关文章
相关标签/搜索