外部ライブラリのpatch
from langchain.chat_models import ChatOpenAI
def sample_func():
fugofugo = ChatOpenAI()
# 〜なんらかの処理が続く
例えばこのsample_func()
関数をテストする場合。
ChatOpenAI
は外部ライブラリのためモック化したいとします。
ダメな例
from sample import sample_func
@patch("langchain.chat_models.ChatOpenAI")
def test_sample_func(mock_thing):
actual = sample_func()
# 以降のテスト内容は省略
これがダメなやつです。
パッチできずに普通に外部ライブラリChatOpenAI
の処理が走ります。
良い例
from sample import sample_func
@patch("sample.ChatOpenAI")
def test_sample_func(mock_thing):
actual = sample_func()
# 以降のテスト内容は省略
これだとChatOpenAI
はモック化された状態でテストできます。
なぜダメなのか?
自分だけかもしれませんが、ダメな例でもいけそうな気がしてしまいます。
なぜダメな例ではパッチできないのか
ダメな例では、langchain.chat_models.ChatOpenAI
をパッチしようとしていますが、これは sample.py
の中で既にインポートされているため、パッチが適用されません。sample.py
は実際には langchain.chat_models.ChatOpenAI
をインポートしており、テスト時にはパッチが適用されていないため、実際の外部ライブラリが実行されてしまいます。
ChatGPTさん↑によると、先にsample.pyの中でlangchain.chat_models.ChatOpenAI
がインポートされるため、後からそれをパッチしても意味がないということらしいです。
抜け道について
ということは、テストコードの中でテスト対象のインポートよりも先にパッチするのであればlangchain.chat_models.ChatOpenAI
をpatchして良いのか聞いてみました。
はい、その通りです。sample.py
のインポートよりも先にパッチを適用すれば、langchain.chat_models.ChatOpenAI
がパッチされるタイミングで、sample.py
でのインポートもパッチされたオブジェクトになります。
提案されたコードは、以下のようになります。
from unittest.mock import patch
@patch("langchain.chat_models.ChatOpenAI")
def test_sample_func(mock_thing):
from sample import sample_func
actual = sample_func()
# 以降のテスト内容は省略
この方法であれば、ダメな例でもパッチが適用されることが期待できます。ただし、テストケース内でインポートを行うことにより、テストコードがやや複雑になることに注意してください。一般的には、sample.ChatOpenAI
のようにモジュール内でインポートされたオブジェクトをパッチすることが推奨されます。
補足:自作モジュールのpatch
自作モジュールをパッチする場合も先ほどの規則に従ってパスを指定する必要があります。
下記で例を書きました。
ディレクトリ構成
以下のようなディレクトリ構成を考えます。
.
└── proj_root/
├── my_lib/
│ └── calc_tool.py
├── many/
│ └── calculate.py
└── main.py
作図:https://tree.nathanfriend.io/
自作モジュールcalc_tool.py
が様々なところで利用されるとします。
def sample_helper():
# 様々なところで使用される自作モジュール
# 外部ライブラリを多様している
上記のsample_helper()
関数は例えばmany
ディレクトリの中のcalculate.py
から以下のように使用されます。
from my_lib.calc_tool import sample_helper
def calclate_something():
something = sample_helper()
# 以降の処理は省略
上記のcalclate_something()
をテストする際に、外部モジュールを多用している部分sample_helper()
関数はモック化しておきたいというケースを考えます。
ダメな例
from many.calculate import calculate_something
from unittest.mock import patch
def test_calc_avarage():
# expected(買い価格)が既知のstock_prices(株価の推移リスト)でテスト
with patch("my_lib.calc_tool.sample_helper") as mock_thing:
actual = calculate_something()
assert actual == expected
これがダメな例。パッチできずに普通にsample_helper()
が実行されています。
良い例
from many.calculate import calculate_something
from unittest.mock import patch
def test_calc_avarage():
# expected(買い価格)が既知のstock_prices(株価の推移リスト)でテスト
with patch("many.calculate.sample_helper") as mock_thing:
actual = calculate_something()
assert actual == expected
これでsample_helper()
がモック化された状態でテストできます。