mocker.patchではまってわかったimportの仕組み

mocker.patchで関数の返り値を指定したかったんだけど、うまくいかずはまった。調べてたらimportが正しく理解できていないことがわかったので、メモ。

コード

utils.py(モック化したいもの)

def utilA():
    return "returnA"

sample.py(テストしたいもの)

from utils import *

def methodA():
    str = utilA()

    return str

test_sample.py(テストコード)

from sample import *

def test_A(mocker):
    mocker.patch('utils.utilA', return_value="mock_returnA")
    print(methodA())

うまくいかなかったこと

関数methodAのテストのため、utilAの戻り値をreturnAではなくmock_returnAにしたかった。 mocker.pathの第1引数はモジュール+オブジェクト名を書けば良い。今回モックにしたいのはutilsモジュールのutilA関数なので utils.utilAであっているはず。

だけどテストを実行すると、

# pytest tests/testsample.py -s
…

tests/testsample.py returnA

と、returnAと出てきてしまった。これは通常通りutilAが実行されてしまっているのでモックが動いていない。なぜなのか。

うまくいった

いろいろ試した結果、mocker.pathの第1引数を sample.utilA と書けば、うまくutilAがモック化されることがわかった。

from sample import *

def test_A(mocker):
    mocker.patch('sample.utilA', return_value="mock_returnA")
    print(methodA())
# pytest tests/testsample.py -s
…

tests/testsample.py mock_returnA

なぜなのか

ポイントはimportだった。自分めんどくさがりなので、自分のモジュールを読み込みたいときはfrom utils import *のようにアスタリスクを使ってインポートすることが多い。 これがどういうことなのかあまり深く考えたことなかったけど、sampleモジュールにutilsモジュールのオブジェクトを展開しているということのようだ。 なのでutilsモジュールのutilAではなくsampleモジュールのutilAを使っているイメージなようだ。 なのでmocker.patchにもmocker.patch('sample.utilA'と書かないと使っているutilAがモック化されない。

なお、PEP8ではこのアスタリスクでのインポートは推奨されていないらしい。

https://pep8-ja.readthedocs.io/ja/latest/#import

ワイルドカードを使った import (from import *) は避けるべきです。なぜなら、どの名前が名前空間に存在しているかをわかりにくくし、コードの読み手や多くのツールを混乱させるからです。

sample.pyのimport文をimport utilsとし、utils.utilAを呼び出すコードにするなら、mocker.patch('sample.utilA'でうまくいく。

sample.py

from utils import 

def methodA():
    str = utils.utilA()

    return str
from sample import *

def test_A(mocker):
    mocker.patch('utils.utilA', return_value="mock_returnA")
    print(methodA())
# pytest tests/testsample.py -s
…

tests/testsample.py mock_returnA