4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【unittest.mock】patchのパス指定でやってしまいがちなミス

Posted at

外部ライブラリのpatch

sample.py
from langchain.chat_models import ChatOpenAI

def sample_func():
    fugofugo = ChatOpenAI()
    # 〜なんらかの処理が続く

例えばこのsample_func()関数をテストする場合。
ChatOpenAIは外部ライブラリのためモック化したいとします。

ダメな例

test_sample.py
from sample import sample_func

@patch("langchain.chat_models.ChatOpenAI")
def test_sample_func(mock_thing):
    actual = sample_func()
    # 以降のテスト内容は省略

これがダメなやつです。
パッチできずに普通に外部ライブラリChatOpenAIの処理が走ります。

良い例

test_sample.py
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でのインポートもパッチされたオブジェクトになります。

提案されたコードは、以下のようになります。

test_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が様々なところで利用されるとします。

calc_tool.py
def sample_helper():
    # 様々なところで使用される自作モジュール
    # 外部ライブラリを多様している

上記のsample_helper()関数は例えばmanyディレクトリの中のcalculate.pyから以下のように使用されます。

calculate.py
from my_lib.calc_tool import sample_helper

def calclate_something():
    something = sample_helper()
    # 以降の処理は省略

上記のcalclate_something()をテストする際に、外部モジュールを多用している部分sample_helper()関数はモック化しておきたいというケースを考えます。

ダメな例

test_stock_calculate.py
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()が実行されています。

良い例

test_stock_calculate.py
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()がモック化された状態でテストできます。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?