偷梁换柱:mockを使う.patch補助pythonユニットテスト
6918 ワード
最近ソフトワークプロジェクトのバックエンドテストをして、pythonの
本グループのプロジェクトは比較的特殊で、教務サイトへの模擬登録と情報の登り取りを設計し、同時に多くのインタフェースにメールを送信するside-effectがある.テストを自動化する際、この2つの機能の行為は生産環境の実際のデータ(ユーザーの教務アカウント、メールアドレス)と結合しているため、専門的なテストプロセスを設計する方法が必要である.考えやすい簡単な考え方は次のとおりです.関連インタフェースにタグテストのブール値パラメータを開き、テスト時に送信され、教務ウェブサイトの関連ロジックを送信/爬取することを遮断し、メール/爬虫類のために個別のテストロジックを設計し、ウェブサイトの主要ロジックのテストとデカップリングする.利点は実現が簡単で、欠点は正常なインタフェースロジックを修正する必要があり、開閉原則に合致せず、処理が適切でないと安全上の危険を招きやすいことである. は、テスト時にこのアカウントを使用して関連機能をテストする専門のテストアカウントを提供します.利点はインタフェースロジックを修正する必要がないことであり、問題は教務を這い出すという需要に対して、提供されたアカウントは真実の学生アカウントであり、自動テスト時に予測できる頻繁に密集したデータ要求はアカウントの正常な使用に影響を与える可能性がある.
上記の2つの考え方を総合すると,メール送信/ウェブサイトをスキップして論理を抽出できるが,バックエンドコード論理を修正する必要がない方法を探すことは容易ではない.pythonは解釈型言語であるため、プログラムの実行時にコードを動的に置き換えるのが非常に便利であるため、テスト時にメールを送信する関数/方法を「偽」関数に置き換えるだけでよい.
詳細は公式ドキュメントを参照してください
より簡明な説明の性質のチュートリアルはAn Introduction to Mocking in Pythonです.
ここでは素早いポイントをまとめてみましょう
まず、おもちゃ関数
下記の2つの書き方が可能です
なお、mockオブジェクト
Mock an item where it is used, not where it came from.
pythonのロードメカニズムは面白いです.関数の場合、mockで指定したモジュールパスが実際に呼び出された場所ではなく定義されている場合、mockはこの関数をロードした他のモジュールを正常に上書きできない可能性があります.
この問題の詳細な説明は公式文書を参照することができ,このStackoverflow質問はいくつかの例を示し,さらなる理解に役立つ.
ここでは,本グループのソフトワークコードのうち
mock.patch
を再復習して、それを使っていくつかの複雑な論理に対するテストを簡略化して、ここで記録します問題の説明
本グループのプロジェクトは比較的特殊で、教務サイトへの模擬登録と情報の登り取りを設計し、同時に多くのインタフェースにメールを送信するside-effectがある.テストを自動化する際、この2つの機能の行為は生産環境の実際のデータ(ユーザーの教務アカウント、メールアドレス)と結合しているため、専門的なテストプロセスを設計する方法が必要である.考えやすい簡単な考え方は次のとおりです.
上記の2つの考え方を総合すると,メール送信/ウェブサイトをスキップして論理を抽出できるが,バックエンドコード論理を修正する必要がない方法を探すことは容易ではない.pythonは解釈型言語であるため、プログラムの実行時にコードを動的に置き換えるのが非常に便利であるため、テスト時にメールを送信する関数/方法を「偽」関数に置き換えるだけでよい.
importlib
などの手段で実現するのは難しくないが、作業量はやや大きく、実際にはpythonはunittest.mock.patch
を提供してこのようなニーズを満たしている.基本的な紹介
詳細は公式ドキュメントを参照してください
より簡明な説明の性質のチュートリアルはAn Introduction to Mocking in Pythonです.
ここでは素早いポイントをまとめてみましょう
使用方法:デザイナまたはコンテキストマネージャ
まず、おもちゃ関数
func_to_test
を与え、この関数は2つのパラメータと1つのオプションパラメータを受信し、2つのパラメータの加算を返し、オプションパラメータの値を印刷する.# author : Mistariano ([email protected])
# file path : pack1/my_module.py
# module name : pack1.my_module
def verbose_adder(arg1, arg2, kwarg1='default'):
print(kwarg1) # side-effect
return arg1 + arg2 # post-condition
def func_to_test():
return verbose_adder(10, 10)
patch
を借りて、hackはverbose_adder
という関数を削除することを望んでいます.テスト時func_to_test
が実際にverbose_adder
に伝達するパラメータが何であるかにかかわらず、その戻り値は3であり、特定の情報の行を出力することが望ましい.下記の2つの書き方が可能です
# author : Mistariano ([email protected])
from pack1.my_module import func_to_test
from unittest import mock
def print_test_info(*args, **kwargs):
print('this is a microphone check.')
print('arguments:', args, kwargs)
return mock.DEFAULT # NOTICE here
@mock.patch('pack1.my_module.verbose_adder')
def test_func_to_test__decorator(mock_obj):
mock_obj.return_value = 3
mock_obj.side_effect = print_test_info
assert func_to_test() == 3
def test_func_to_test__context():
with mock.patch('pack1.my_module.verbose_adder') as mock_obj:
mock_obj.return_value = 3
mock_obj.side_effect = print_test_info
assert func_to_test() == 3
mock.patch
は、装飾された関数が追加のパラメータを提供してmockオブジェクトインスタンスmock_obj
を受信する必要がある関数装飾またはコンテキストマネージャとして使用することができ、後者はmockオブジェクトインスタンスをコンテキストマネージャの戻り値として使用することができる.もちろん,直接関数として呼び出すのも望ましいが,個人的には推奨されず,ここでは詳細には論じない.mock_obj
に戻り値(オプション)と副作用(オプション)を指定することによってmock関数の動作をカスタマイズし、元の関数/メソッドの動的オーバーライドを実現するなお、mockオブジェクト
side_effect
として使用されるコールバック関数の戻り値はmock.DEFAULT
であり、これは、別途定められたreturn_value
を上書きしないようにするためであるどの関数にpatchを打つべきですか
Mock an item where it is used, not where it came from.
pythonのロードメカニズムは面白いです.関数の場合、mockで指定したモジュールパスが実際に呼び出された場所ではなく定義されている場合、mockはこの関数をロードした他のモジュールを正常に上書きできない可能性があります.
この問題の詳細な説明は公式文書を参照することができ,このStackoverflow質問はいくつかの例を示し,さらなる理解に役立つ.
実戦
ここでは,本グループのソフトワークコードのうち
patch
を用いてメール送信および教務爬取を上書きするコードセグメントを直接与える.from django.test import TestCase
from unittest import mock
# ...
class ViewTestCases(TestCase):
# ...
@staticmethod
def mock_mail_send(*args, **kwargs):
print('sending mock mail.. args:', args, kwargs)
return mock.DEFAULT
@staticmethod
def mock_update_from_course(*args, **kwargs):
print('mock updating course... args:', args, kwargs)
return mock.DEFAULT
def _test_req_context(self, func, exp_code, auth_required):
def test_req_wrapper(*args, **kwargs):
token = None if not self._user_data else self._user_data['token']
with mock.patch('ddl_killer.utils.sendmail.YAG.send') as mail_obj:
mail_obj.side_effect = self.mock_mail_send
with mock.patch('ddl_killer.views.updateFromCourse') as mock_course:
mock_course.side_effect = self.mock_update_from_course
mock_course.return_value = self.TEST_COURSE
if auth_required:
r_data = func(*args, HTTP_AUTHORIZATION=None, **kwargs).json()
self.assertEqual(r_data['code'], 401, r_data)
r_data = func(*args, HTTP_AUTHORIZATION=token, **kwargs).json()
self.assertEqual(r_data['code'], exp_code)
return r_data
return test_req_wrapper
def post(self, *args, exp_code=200, auth_required=True, **kwargs):
return self._test_req_context(self._client.post, exp_code, auth_required)(*args, **kwargs)
def get(self, *args, exp_code=200, auth_required=True, **kwargs):
return self._test_req_context(self._client.get, exp_code, auth_required)(*args, **kwargs)
def _login(self):
if self._user_data is None:
print('logging...')
r = self.post('/api/login',
{'uid': self.TEST_USER_ID,
'password': self.password_encrypt},
auth_required=False)
self._user_data = r
def test_show_user(self):
self._login()
data = self.post('/api/user/{}/info'.format(self.TEST_USER_ID))
self.assertEqual(data['uid'], self.TEST_USER_ID)
self.assertEqual(data['name'], self.TEST_USER_NAME)
self.assertEqual(data['email'], self.TEST_USER_EMAIL)
def test_user_login_not_activated(self):
self._user_orm.is_active = False
self._user_orm.save()
r = self.post('/api/login', {'uid': self.TEST_USER_ID,
'password': self.password_encrypt},
exp_code=400,
auth_required=False)
self._user_orm.is_active = True
self._user_orm.save()
def test_edit_user(self):
self._login()
data = self.post('/api/modify', {
'uid': self.TEST_USER_ID,
'name': self.TEST_USER_NAME,
'password': '',
'email': '[email protected]'
})
self.assertEqual(User.objects.get(uid=self.TEST_USER_ID).email,
'[email protected]')
self._user_orm.email = self.TEST_USER_EMAIL
self._user_orm.save()
# ...