使用python mock测试子模块中的函数的最佳实践

时间:2017-08-22 21:22:17

标签: python unit-testing mocking python-mock

所以, 考虑我有一个简单的库,我正在尝试编写单元测试。该库与数据库通信,然后使用该数据调用SOAP API。我有三个模块,每个模块都有一个测试文件。

dir结构:

./mypkg
    ../__init__.py
    ../main.py
    ../db.py
    ../api.py

./tests
    ../test_main
    ../test_db
    ../test_api

代码:

#db.py
import mysqlclient
class Db(object):
    def __init__(self):
        self._client = mysqlclient.Client()

    @property
    def data(self):
        return self._client.some_query()


#api.py
import soapclient
class Api(object):
    def __init__(self):
        self._client = soapclient.Client()

    @property
    def call(self):
        return self._client.some_external_call()


#main.py
from db import Db
from api import Api

class MyLib(object):
    def __init__(self):
        self.db = Db()
        self.api = Api()

    def caller(self):
        return self.api.call(self.db.data)

单元测试:

#test_db.py
import mock
from mypkg.db import Db

@mock.patch('mypkg.db.mysqlclient')
def test_db(mysqlclient_mock):
    mysqlclient_mock.Client.return_value.some_query = {'data':'data'}
    db = Db()
    assert db.data == {'data':'data'}


#test_api.py
import mock
from mypkg.api import Api

@mock.patch('mypkg.db.soapclient')
def test_db(soap_mock):
    soap_mock.Client.return_value.some_external_call = 'foo'
    api = Api()
    assert api.call == 'foo'

在上面的示例中,mypkg.main.MyLib拨打mypkg.db.Db()(使用第三方mysqlclient),然后mypkg.api.Api()(使用第三方soapclient

我使用mock.patch修补第三方库,分别在test_dbtest_api模拟我的数据库和api调用。

现在我的问题是,是否建议在test_main中修补这些外部呼叫,或者只是修补db.Dbapi.Api? (这个例子非常简单,但是在较大的库中,在再次修补外部调用或甚至使用修补内部库的测试助手函数时,代码变得很麻烦。)

选项1:再次在main中修补外部库

#test_main.py
import mock
from mypkg.main import MyLib

@mock.patch('mypkg.db.mysqlclient')
@mock.patch('mypkg.api.soapclient')
def test_main(soap_mock, mysqlcient_mock):
    ml = MyLib()
    soap_mock.Client.return_value.some_external_call = 'foo'
    assert ml.caller() == 'foo'

选项2:修补内部库

#test_main.py
import mock
from mypkg.main import MyLib

@mock.patch('mypkg.db.Db')
@mock.patch('mypkg.api.Api')
def test_main(api_mock, db_mock):
    ml = MyLib()
    api_mock.return_value = 'foo'
    assert ml.caller() == 'foo'

1 个答案:

答案 0 :(得分:5)

mock.patch创建导入的模拟版本,而不是它所在的位置。这意味着传递给mock.patch的字符串必须是被测模块中导入模块的路径。以下是test_main.py中补丁装饰器应该是什么样子:

@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')

此外,修补模块(api_mockdb_mock)上的句柄指的是,而不是这些类的实例。当你写api_mock.return_value = 'foo'时,你会告诉api_mock返回“foo”#39;当它被调用时,而不是当它的实例有一个调用它的方法时。以下是main.py中的对象以及它们与您的测试中的api_mockdb_mock的关系:

Api is a class                     : api_mock
Api() is an instance               : api_mock.return_value
Api().call is an instance method   : api_mock.return_value.call
Api().call() is a return value     : api_mock.return_value.call.return_value

Db is a class                      : db_mock
Db() is an instance                : db_mock.return_value
Db().data is an attribute          : db_mock.return_value.data
因此,

test_main.py应如下所示:

import mock
from mypkg.main import MyLib

@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')
def test_main(api_mock, db_mock):
    ml = MyLib()

    api_mock.return_value.call.return_value = 'foo'
    db_mock.return_value.data = 'some data' # we need this to test that the call to api_mock had the correct arguments.

    assert ml.caller() == 'foo'
    api_mock.return_value.call.assert_called_once_with('some data')

选项1中的第一个补丁对于单元测试db.py非常有用,因为它为db模块提供了mysqlclient的模拟版本。同样,@mock.patch('mypkg.api.soapclient')属于test_api.py

我无法想到选项2可以帮助您进行单元测试的方式。

编辑:我错误地将类称为模块。 db.py和api.py是模块