使用 python mock 测试子模块内功能的最佳实践
Best practices using python mock for testing functions within sub modules
所以,
考虑一下我有一个简单的库,我正在尝试为其编写单元测试。该库与数据库对话,然后使用该数据调用 SOAP API。我有三个模块,每个模块都有一个测试文件。
目录结构:
./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
修补第三方库以模拟我的数据库和 api 分别调用 test_db
和 test_api
。
现在我的问题是,建议在 test_main
中再次修补这些外部调用还是简单地修补 db.Db
和 api.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'
mock.patch
在导入 的地方创建某物的模拟版本,而不是它存在的地方。这意味着传递给 mock.patch
的字符串必须是被测模块中导入模块的路径。这是 test_main.py
:
中补丁装饰器的样子
@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')
此外,您在修补模块(api_mock
和 db_mock
)上的句柄指的是 类,而不是实例那些 类。当你写 api_mock.return_value = 'foo'
时,你是在告诉 api_mock 到 return 'foo' 当它被调用时,而不是当它的实例有一个方法调用它时。以下是 main.py 中的对象以及它们与测试中的 api_mock
和 db_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 是模块
所以, 考虑一下我有一个简单的库,我正在尝试为其编写单元测试。该库与数据库对话,然后使用该数据调用 SOAP API。我有三个模块,每个模块都有一个测试文件。
目录结构:
./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
修补第三方库以模拟我的数据库和 api 分别调用 test_db
和 test_api
。
现在我的问题是,建议在 test_main
中再次修补这些外部调用还是简单地修补 db.Db
和 api.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'
mock.patch
在导入 的地方创建某物的模拟版本,而不是它存在的地方。这意味着传递给 mock.patch
的字符串必须是被测模块中导入模块的路径。这是 test_main.py
:
@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')
此外,您在修补模块(api_mock
和 db_mock
)上的句柄指的是 类,而不是实例那些 类。当你写 api_mock.return_value = 'foo'
时,你是在告诉 api_mock 到 return 'foo' 当它被调用时,而不是当它的实例有一个方法调用它时。以下是 main.py 中的对象以及它们与测试中的 api_mock
和 db_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 是模块