はじめに
pythonとユニットテストについて学び始めたのでメモを残す。
誤りや意見などあれば、是非お願いします。
今回は、os
モジュールを例にモックするパターンを考えていこうと思う。
大きく2つに分けて、下記パターンを考える。
-
mocker.patch()
でモジュールの対象メソッドをモックする -
mocker.patch.object()
でモジュールの対象メソッドをモックする - (オプション)
monkeypatch.setattr()
でモックする
また、from ~ import
を利用する場合の考慮点も書いてみる。
boto3 モジュールのモックについても余力があれば書きたい。
学習する中で思ったこと
pythonのユニットテストは、そこそこ種類(名前が違うだけで動作はほぼ同じはず)があり混乱した。
このstackoverflowの内容に共感した。
テスト関係のモジュール
- pytest
- pytest-mock
確認したこと
とりあえず、実装パターンを下記に示す。
どれでもos.path.join
をモックできているはず。。。
モジュールをモックする場合の使い分けが理解できていない。(違いについて教えてくれると喜びます。
mocker.patch()
でモジュールの対象メソッドをモックする
テストの意味は考えず、適当なコードで確認する。
- hoge.py: テスト対象ファイル
- test_hoge.py: テストコードファイル
# test_hoge.py
import pytest
import hoge
@pytest.fixture
def mock_os_path_join(mocker):
# joinをパッチ
mocker.patch('os.path.join', return_value='/x/y/z') # mocker.patchを使用
def test_hoge(mock_os_path_join):
path = hoge.func('a/b')
assert path == '/x/y/z'
# hoge.py
import os
def func(dir):
path = os.path.join(dir, 'b/d')
return path
mocker.patch.object()
でモジュールの対象メソッドをモックする
# test_hoge.py
import pytest
import hoge
import os
@pytest.fixture
def mock_os_path_join(mocker):
# joinをパッチ
mocker.patch.object(os.path, 'join', return_value='/z/y/x') # mocker.patch.objectを使用
def test_hoge(mock_os_path_join):
path = hoge.func('a/b')
assert path == '/z/y/x'
(オプション)monkeypatch.setattr()
でモックする
# test_hoge.py
import pytest
from pytest_mock import mocker
import hoge
import os
@pytest.fixture
def mock_os_path_join(monkeypatch):
def mock_join(*args, **kwargs):
return '/z/y/x'
# monkeypatch.setattrを使用
monkeypatch.setattr(os.path, 'join', mock_join)
# monkeypatch.setattr(os.path, 'join', mocker.Mock(return_value='/z/y/x'))
# monkeypatchにMockオブジェクトを使うことで、return_valueを動的にセット可能
# mocker単体で上記仕組みは実装できるので、今回だとmonkeypatchを使用する意味はほぼないと思う
def test_hoge(mock_os_path_join):
path = hoge.func('a/b')
assert path == '/z/y/x'
Mockオブジェクトを使うと、return_value
やside_effect
で色々できるのが便利だと思う。
from ~ importを使用したモジュールをモックする場合
個人的にこのパターンは面倒くさいと思うのでメモする。
from os.path import join
こんな感じでインポートしたとき、テストコード側も変わる。
対象のモジュールがオブジェクトとしてインポートされていることを意識することが大事。
from import によって利用可能になったモジュール変数はもとのモジュールから切り離されるため、 もとのモジュールをパッチしても書き換えられません。
# hoge2.py
from os.path import join
print(f'joinオブジェクトが属するモジュール名: {__name__}')
def func(dir):
path = join(dir, 'b/d')
# print(path)
return path
mocker.patchを使用
# test_hoge2.py
import pytest
import hoge2
@pytest.fixture
def mock_os_path_join(mocker):
# hoge2.pyでインポートしたjoinオブジェクトが属するモジュールを指定する必要がある
mocker.patch('hoge2.join', return_value='/x/y/z')
def test_hoge(mock_os_path_join):
path = hoge2.func('a/b')
assert path == '/x/y/z'
mocker.patch.objectを使用。
fixtureでhoge3をimportする必要はなかったと思う。
# test_hoge3.py
import pytest
@pytest.fixture
def hoge3():
import hoge3
return hoge3
@pytest.fixture
def mock_os_path_join(mocker, hoge3):
mocker.patch.object(hoge3.path, 'join', return_value='/z/y/x')
def test_hoge(mock_os_path_join, hoge3):
path = hoge3.func('a/b')
assert path == '/z/y/x'
# hoge3.py
from os import path
def func(dir):
ret = path.join(dir, 'b/d')
return ret
boto3モジュールをモックする場合
そもそもmoto
モジュールがモック対象をサポートしているなら、moto
を使うほうがよいはず。
今回は、mocker.patch
系統を使う場合について考える。
どのレベルでモックするべきか迷ったので、メモする。
boto3.client()
のclient
をモックする
この単位でモックするのは好ましくないと思う。client
関数全体をモックすると、他への影響が大きそう。
# app.py
import boto3
translate_cliet = boto3.client('translate')
def main():
print(translate_cliet)
res = translate_cliet.translate_text(
Text='おはよう',
SourceLanguageCode='ja',
TargetLanguageCode='en',
)
print(res)
return res
# test_app.py
import boto3
import pytest
@pytest.fixture
def app():
import app
return app
@pytest.fixture
def mock_boto3_client(mocker):
mock_client = mocker.Mock()
mock_client().translate_text.return_value = '翻訳結果のオブジェクト'
mocker.patch('boto3.client', mock_client)
def test_app(mock_boto3_client, app):
assert app.main() == '翻訳結果のオブジェクト'
boto3.client()
から生成したオブジェクトをモックする
app.pyのtranslate_client
変数をモックしているので、範囲が最小限。
import boto3
import pytest
@pytest.fixture
def app():
import app
return app
@pytest.fixture
def mock_boto3_client(mocker, app):
translate_client = boto3.client('translate')
mock_client = mocker.Mock(wraps=translate_client)
mock_client.translate_text.return_value = '翻訳結果のオブジェクト'
mocker.patch.object(app, 'translate_cliet', mock_client)
def test_app(mock_boto3_client, app):
assert app.main() == '翻訳結果のオブジェクト'
おわりに
単語の理解が曖昧だと思った。「モックする」と「パッチする」のどっちが正しいのか、書きながらわからなくなったので後で整理する。
Mock
とMagicMock
の違いや、キーワード引数の動作についても調べる必要があると感じる。
最後に、pytestを学習する際は一緒にunittest.mockを調べるのが大事だと思った。