モックしたい(unittest.mock)
1from unittest.mock import MagicMock
2from unittest.mock import patch
3from unittest.mock import mock_open
unittestはPythonの標準モジュールです。
unittest.mockは、そこに含まれているモック用のモジュールです。
注釈
ユニットテストにpytestを使っている場合は、pytest-mockを使うとよいです。
MagicMockしたい(MagicMock)
1from unittest.mock import MagicMock
2
3mock = MagicMock()
4mock.メソッド名.return_value = 返り値
5mock.メソッド名.return_value = [返り値]
MagicMockでモック用のオブジェクトを作成できます。
モックは万能の空箱のようなイメージで、任意のメソッドを追加して、その返り値を設定できます。
ヒント
ユニットテストを作成するとき、どの部分をモックするかを考えることが重要です。 経験が圧倒的に不足していて悩んでいましたが、最近では、ChatGPTに聞きながら作っています。
パッチしたい(@patch)
1from unittest.mock import patch
2
3@patch("モジュール名.クラス名")
4def test_テスト関数(モック名):
5 """ユニットテストの説明"""
6 # テストを書く
7 # モック名.メソッド名.return_value = モック値
@patchデコレーターで引数に指定した関数をモックできます。
複数パッチしたい(@patch)
1# @デコレーターの順番と引数の順番の対応に注意
2@patch("モジュール名.クラス名3") # => モック名3 で受け取る
3@patch("モジュール名.クラス名2") # => モック名2 で受け取る
4@patch("モジュール名.クラス名1") # => モック名1 で受け取る
5def test_テスト関数(モック名1, モック名2, モック名3):
6 """複数のモックを使ったテスト"
7
8 モック名1.メソッド名.return_value = ...
9 モック名2.メソッド名.return_value = ...
10 モック名3.メソッド名.return_value = ...
ひとつのテスト関数に、複数の@patchデコレーターを使用できます。
テスト用の関数の引数で、それぞれのモックを受け取ることができます。
デコレーターの順番と引数の順番の対応に注意が必要です。
パッチしたい(patch)
1def test_テスト関数():
2 with patch("モジュール名.クラス名") as モック名:
3 # テストを書く
@patchデコレーターは、patch関数としてコンテキストマネージャーのように使うことができます。
1def test_download():
2 with patch("subprocess.run") as mock_subprocess_run:
3 url = TEST_SHARED_URL
4 sheet = Sheet(...)
5 sheet.download()
6 mock_subprocess_run.assert_called_with(...)
シリアル通信をモックしたい(MagicMock)
def read_event(port: serial.Serial) -> list[str]:
data = port.readline().decode("UTF-8").strip().split()
return data
シリアル通信でデータを取得するためのread_eventという関数です。
USB接続がない状態でユニットテストできるようにします。
from unittest.mock import MagicMock
from .daq import read_event
def test_daq_read_event():
# Arrange: モックを作成
mock_port = MagicMock
mock_port.readline().decode.return_value = "値1 値2 値3 値4"
# Act: データ読み込みを実行
data = read_event(port)
# Assert: 型の一致を確認
assert isinstance(data, list)
serial.SerialオブジェクトをMagicMockすることで、
USB接続がない状態でread_eventのユニットテストできます。
またread_eventではreadline()でデータを取得しています。
検出器が返すデータ形式がスペース区切りの文字列になっているため
mock_port.readline().decode.return_value = "値1 値2 ..."としています。
ただし、実際の検出器からのデータは、ある程度ランダムな数値です。 そのため、返ってくる値そのものを検証する意味はありません。 ここでは、得られたオブジェクトの型が正しいかどうかで検証することにしました。
Tip
実際の改良としてFakeEventクラスを作成し、
mock_port.readline().decode.return_value = FakeEvent().to_tsv_string()でモックしました。
FakeEventクラスは、擬似データを生成するためのクラスで、測定データが取りうる範囲からランダムな値を返します。
ファイル処理をモックしたい(mock_open)
1from unittest.mock import MagicMock, mock_open
2
3@patch("pathlib.Path.open", new_callable=mock_open)
4def test save_events(mock_open):
5 # Arrange: ファイルをモック
6 fname = Path("remove_this_file_if_exists.csv")
7
8 # Act: ファイルを開き、データを追記する
9 n = 5
10 events = []
11 with fname.open("x") as f:
12 for _ in range(n):
13 event = read_event(port) # 上でモックした値
14 f.write(event + "\n")
15 events.append(event)
16
17 # Assert
18 ## 取得したイベント数を確認
19 assert len(events) == n
20
21 ## ファイルを開いたモードと回数を確認
22 mock_open.assert_called_once_with("x")
23
24 ## writeが呼ばれた回数を確認
25 handle = mock_open()
26 assert handle.write.call_count == n
mock_openで、実際にファイルを作ったり、書き込んだりせずに、ファイル処理をテストできます。
ファイルは作成されないので、
ファイルが開けたかどうか、read/write処理が想定回数呼び出せたかどうか、などでアサートします。
このサンプルでは@patchデコレータで、pathlib.Path.openをモックしています。
この関数のスコープの中で呼ばれるopen関数は、mock_openに置き換えられています。
mock_openもMagicMockと同じように関数/メソッドを呼び出した回数を記録しています。
そのため関数名.call_countでアサートできます。