このBlogは移転しました。今後は aish.dev を御覧ください。

めんどくさいmock.patch()

unittest.mock モジュールを正しく使って関数を置き換えるというのは以外と難しいもので、Python名前空間について、しっかり把握できてないとうまくいかないことがある。

単純なケースでは、テスト対象のコードが参照している名前で置き換えてやればいい。 例えば

import spam

def ham():
    spam.egg()

というモジュール Mham() をテストするために spam.egg を置き換えるなら

def test():
    import M
    with patch("spam.egg"):
        M.ham()

となる。また、

from spam import egg

def ham():
    egg()

のように egg を参照している場合、ham() の内部での eggM.egg への参照なので

def test():
    import M
    with patch("M.egg"):
        M. ham()

となる。

ここで注意しなければならないのが、関数を置き換える対象となるモジュールは、関数を呼び出すときに指定するモジュールではなく、関数が定義されたモジュールだということだ。

例えばもう一つのモジュール、M2 があって、

import M
ham2 = M.ham

となっている場合がある。こんな場合でも、M2.ham2() で呼び出される egg() を置き換えるには

def test():
    import M2
    with patch("M.egg"):
        M2.ham2()

のように、ham を定義したモジュール Megg を置き換えなければならない。

このように、機械的に mock.patch() を使って置き換えることはできず、置き換える対象の関数がどのように呼び出されているか、ちゃんと調べてなくてはならない。普通はあまり気にしなくても良いのだが、たまに変な import などでどのモジュールを使っているのか調べにくいパッケージなどもある。そういう時は、

@contextlib.contextmanager
def patch_globalref(func, target):
   m = MagicMock()
      with patch.dict(sys.modules[func.__module__].__dict__, **{target:m}):
             yield m

のような関数を用意しておいて、

def test():
    import M2
    with patch_globalref(M2.ham2, 'egg'):
       M2.ham2()

とように、テスト対象の関数オブジェクトのグローバルスコープを直接修正してしまうほうが手っ取り早い場合もある。