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

# 外部ライブラリのpatch
```python: sample.py
from langchain.chat_models import ChatOpenAI

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

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

## ダメな例
```python: test_sample.py
from sample import sample_func

@patch("langchain.chat_models.ChatOpenAI")
def test_sample_func(mock_thing):
    actual = sample_func()
    # 以降のテスト内容は省略
```
これがダメなやつです。
パッチできずに普通に外部ライブラリ`ChatOpenAI`の処理が走ります。

## 良い例
```python: test_sample.py
from sample import sample_func

@patch("sample.ChatOpenAI")
def test_sample_func(mock_thing):
    actual = sample_func()
    # 以降のテスト内容は省略
```
これだと`ChatOpenAI`はモック化された状態でテストできます。

## なぜダメなのか？
自分だけかもしれませんが、ダメな例でもいけそうな気がしてしまいます。

:::note info
**なぜダメな例ではパッチできないのか**
ダメな例では、`langchain.chat_models.ChatOpenAI`をパッチしようとしていますが、これは `sample.py`の中で既にインポートされているため、パッチが適用されません。`sample.py`は実際には `langchain.chat_models.ChatOpenAI`をインポートしており、テスト時にはパッチが適用されていないため、実際の外部ライブラリが実行されてしまいます。
:::

ChatGPTさん↑によると、先にsample.pyの中で`langchain.chat_models.ChatOpenAI`がインポートされるため、後からそれをパッチしても意味がないということらしいです。

### 抜け道について

ということは、テストコードの中でテスト対象のインポートよりも先にパッチするのであれば`langchain.chat_models.ChatOpenAI`をpatchして良いのか聞いてみました。

:::note info
はい、その通りです。`sample.py`のインポートよりも先にパッチを適用すれば、`langchain.chat_models.ChatOpenAI`がパッチされるタイミングで、`sample.py`でのインポートもパッチされたオブジェクトになります。

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

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

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

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

```python:calculate.py
from my_lib.calc_tool import sample_helper

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

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

## ダメな例
```python: 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()`が実行されています。

## 良い例
```python: 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()`がモック化された状態でテストできます。