2017-08-22 17 views
-1

だから、 を考えてください。単体テストの作成を試みている単純なライブラリがあります。このライブラリはデータベースと通信し、そのデータを使用してSOAP APIを呼び出します。私は3つのモジュールと各モジュールのテストファイルを持っています。サブモジュール内の関数をテストするためのPythonモックを使用したベストプラクティス

ディレクトリ構造:

./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は(サードパーティmysqlclientを使用)mypkg.db.Db()を呼び出しmypkg.api.Api()(サードパーティのを使用))

私は別にtest_dbtest_apiに私のDBとのAPI呼び出しを模擬するために、サードパーティのライブラリにパッチを適用するmock.patchを使用しています。

私の質問は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' 

答えて

2

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'が返され、インスタンスに呼び出されたメソッドがある場合は返されません。ここでは、オブジェクトはmain.pyであり、彼らはあなたのテストでapi_mockdb_mockにどのように関係するか:

Api is a module     : 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 module      : 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-moduleにmysqlclientのモックバージョンを与えるので、ユニットテストのためにうまくいきます。db.py同様に、@mock.patch('mypkg.api.soapclient')test_api.pyに属します。

私は方法2を考えることができません。オプション2は何かをユニットテストするのに役立ちます。

関連する問題