使用mock模块来帮助python测试

相比较于unittest,nose这类测试类库,mock给我的感觉是完全不一样的。刚开始看mock是因为openstack中的测试用到了它,翻阅其文档时候完全不清楚他是干嘛的。直到看到了 http://www.oschina.net/translate/unit-testing-with-the-python-mock-class 这篇文章,才发现mock可以解决我很多实际的问题。这里小秦就记录下mock的实际使用场景的一个例子,方便和我之前有同样疑问的人能快速的理解其价值。

另外,强烈推荐看下http://www.oschina.net/translate/unit-testing-with-the-python-mock-class这个文章,他写的比我下面要写的详细的多。同时mock的官方文档也是强烈推荐值得一看的。

首先说下mock是干什么的。在一些测试用例中,测试会的依赖一些外部资源,这些资源一般通过封装的类或方法进行访问。比如我有个方法是删除一台虚拟机,然后根据删除所消耗的时间判定是否需要再次建立虚拟机:

def recreate_vm():
    delete_used_time = VMUtil.delete_vm(XXX)    #删除一台VM,测试的时候我们知道目前的测试资源删除一台虚拟机耗时肯定大于100秒
    XXXXXX......  #这里是其它的一些逻辑代码
    if delete_used_time > 100:
        VMUtil.create_vm(delete_used_time)  #如果耗时大于100,则重新建立VM,并且将这个参数传入
        #如果代码运行到这边,那么就ok
        print 'ok'

问题来了,虚拟机的创建和删除是比较耗时的,有时候可能还需要花钱,对于软件开发测试来说,每次测试都跑这么一段代码代价很大。那怎么才能判断recreate_vm这个方法是不是工作正常呢?我们可以假设VMUtil这个方法是完全ok的,可以信任的,所以我们要测试的其实是我们的‘XXXXXX…… ’这段逻辑,以及最后代码是否运行到了‘VMUtil.create_vm(delete_used_time)’并且是否正确传入了参数。因此我们可以试着用下面的例子替代:

def __delete_vm(XXX):
    return 500

def __create_vm(XXX):
    pass

VMUtil.delete_vm = __delete_vm
VMUtil.create_vm = __create_vm

def recreate_vm():
    delete_used_time = VMUtil.delete_vm(XXX)
    XXXXXX......  #这里是其它的一些逻辑代码
    if delete_used_time > 100:
        VMUtil.create_vm(delete_used_time)
        print 'ok'

def test_recreate_vm():
    recreate_vm()

此时再跑这段代码,很快速的就能知道代码是不是运行到了if中(如果打印ok那么就是运行到了if中)。这个例子其实就是一种mock,在外部测试资源比较昂贵的情况下,如果其和我们要真正测试的代码逻辑无关,那么通过某种仿造的方式取代外部测试资源的调用,从而实现我们对真正要测试的逻辑代码的测试。但这里有个问题,如果原来的测试方法没有print ‘ok’这一行,我们怎么知道代码运行到了if中呢?另外我们怎么知道在XXXXXX这段逻辑代码执行完后,我们的delete_used_time是否还是VMUtil.delete_vm返回的那个值呢?这个时候就可以用mock了。

用mock这个模块的话上面的例子可以写成:

def recreate_vm():
    delete_used_time = VMUtil.delete_vm(XXX)
    XXXXXX......  #这里是其它的一些逻辑代码
    if delete_used_time > 100:
        VMUtil.create_vm(delete_used_time)
        print 'ok'

@mock.patch.object(VMUtil, 'delete_vm')
@mock.patch.object(VMUtil, 'create_vm')
def test_recreate_vm(create_vm_mock, delete_vm_mock):
    delete_vm_mock.return_value = 500   #这行代码保证VMUtil.delete_vm的调用返回500
    recreate_vm()
    VMUtil.create_vm.assert_called_once_with(500)   #这行代码保证我们的if中的create_vm会的被调用,并且保证我们的delete_used_time没有因为XXXXX的逻辑而改变

这就是mock的一个作用:在外部资源很昂贵,或者不大容易知道测试代码的运行结果的时候,可以自动化的进行测试用例的测试。小秦个人觉得assert_called_once_with这类方法给我的感受是最新鲜的,delete_vm_mock.return_value = 500通过上面的例子其实可以直接实现,但是要判断代码是否运行到if以及create_vm是否正确被调用这个,用mock真的是很方便。

再比如下面的例子,我们的测试环境压根就没有数据库,但是我们要做和数据库相关的测试:

def operation_on_db():
    name = db.exec_sql('select name from XXX limit 1')
    parsed_name = do_sth_on_name(name)  #应该得到name * 2
    age = db.exec_sql('select age from YYY limit 1')
    parsed_age = do_sth_on_age(age) #应该得到age + 1
    XXXXXX......    #一些逻辑代码
    return '{name} - {age}'.format(name=parsed_name, age=parsed_age)

通过用mock,我们可以很容易的在没有db的情况下进行测试:

@mock.patch.object(db, 'exec_sql')
def test_operation_on_db(exec_sql_mock):
    exec_sql_mock.side_effect = ['name_xxx', 23]
    ret = operation_on_db()
    assert ret == 'name_xxxname_xxx - 24'

4 Responses

  1. MatheMatrix 2015年1月5日 / 上午10:30

    第一个例子算是 monkey patch 吧

    • thuanqin 2015年1月9日 / 上午12:24

      恩,是monkey patch :)。最近特意将一些测试用例改用mock,效果还是很好的,不过对c实现的一些模块用起来还是有些不方便。

  2. Mark 2015年2月4日 / 上午10:26

    谢谢你的文章,对我很有帮助,方便留个邮箱做一些技术交流吗

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*