Python の mock.patch のハマりやすい挙動についてまとめる


mock.patch を使って一部のモジュールをモックに差し替えてテストをする際に、モックに変えたい箇所が思った通りに適用されずにハマったことってありませんか?
この記事では mock.patch を使ってモジュールをモックに差し替える時に、 patch によってどこがモックに変わるのか挙動を見てみましょう。

TL;DR

  • import hoge と import されているモジュールの hoge.fuga を patch するには hoge.fuga を指定する
  • from hoge import fuga と import されている fuga を patch するには <import 文を書いている方のモジュール名>.fuga を指定する

動作確認用のモジュールを用意する

patch の動作を確かめるためにまずこのようなモジュールを用意します。

some_module.py
import os
from os import listdir


def some_func():
    print('os.listdir(): ', os.listdir('.'))
    print('listdir(): ', listdir('.'))

図で書くとこんなイメージです。

そして some_func() を実行する以下のようなテストを用意します。

test.py
from some_module import some_func

class TestCase(unittest.TestCase):
    def test_some_func(self):
        some_func()

このテストを実行してみると以下のような出力になります。

テストの実行結果
$ python -m unittest test.py 
os.listdir():  ['__pycache__', 'test.py', 'some_module.py']
listdir():  ['__pycache__', 'test.py', 'some_module.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

当然ですが some_func() の中で実行されている os.listdir()listdir() はどちらも同じ結果を返していることがわかりますね。

1. patch による動作を確認する

os.listdir をモックに差し替えてテストしたい場合、対象のモジュールがどのように import されているかによって patch で指定する対象を変える必要があります。

1-1. os.listdir を patch してみる

ここでテストの内容を以下のように書き換えて os.listdir<return from mock> という文字列を返すようにモックに差し替えてみます。

test.py
import unittest
from unittest.mock import patch

from some_module import some_func


class TestCase(unittest.TestCase):
    @patch('os.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()

これを実行すると以下の出力になります。

テストの実行結果
$ python -m unittest test.py 
os.listdir():  <return from mock>
listdir():  ['__pycache__', 'test.py', 'some_module.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

os.listdir() の処理だけがモックに差し変わっていることがわかります。
os.listdir の参照先がモックに差し代わり、 some_module.listdir は影響を受けません。

1-2. some_module.listdir を patch してみる

今度は some_module.listdir<return from mock> という文字列を返すモックに差し替えてみましょう。

test.py
import unittest
from unittest.mock import patch

from some_module import some_func


class TestCase(unittest.TestCase):
    @patch('some_module.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()

これを実行すると以下の出力になります。

テストの実行結果
$ python -m unittest test.py 
os.listdir():  ['__pycache__', 'test.py', 'some_module.py']
listdir():  <return from mock>
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

今度は listdir() の処理だけがモックに差し変わっていることがわかりますね。
今度は some_module.listdir の参照先がモックに差し変わるので、os.listdir の参照先は影響を受けません。

2. 複数のモジュールから利用されているモジュールを patch する

さて、今度は os.listdir が複数のモジュールから利用されている場合に patch の対象によってどのような挙動になるか見てみましょう。

os.listdir を使うモジュール some_module.pysome_module_2.py を用意します。

some_module.py
import os
from os import listdir


def some_func():
    print('some_func', 'os.listdir(): ', os.listdir('.'))
    print('some_func', 'listdir(): ', listdir('.'))
some_module_2.py
import os
from os import listdir


def some_func_2():
    print('some_func_2', 'os.listdir(): ', os.listdir('.'))
    print('some_func_2', 'listdir(): ', listdir('.'))

図で書くとこんな感じです。

2-1. os.listdir を patch する

テストから some_func() some_func_2() の両方が実行されるようにして、 os.listdir を patch してみます。

test.py
import unittest
from unittest.mock import patch

from some_module import some_func
from some_module_2 import some_func_2


class TestCase(unittest.TestCase):
    @patch('os.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
        some_func_2()

これを実行すると以下の出力になります。

実行結果
$ python -m unittest test.py 
some_func os.listdir():  <return from mock>
some_func listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func_2 os.listdir():  <return from mock>
some_func_2 listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

some_module some_module_2 の両方で os.listdir() の処理がモックに差し変わっていることがわかりますね。
os.listdir の参照先がモックに差し代わることで、some_module some_module_2 の両方とも影響を受けます。

2-2. some_module.listdir を patch する

今度は some_module.listdir を patch します。

test.py
import unittest
from unittest.mock import patch

from some_module import some_func
from some_module_2 import some_func_2


class TestCase(unittest.TestCase):
    @patch('some_module.listdir')
    def test_some_func(self, mock):
        mock.return_value = '<return from mock>'
        some_func()
        some_func_2()

これを実行すると以下の出力になります。

実行結果
$ python -m unittest test.py 
some_func os.listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func listdir():  <return from mock>
some_func_2 os.listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
some_func_2 listdir():  ['__pycache__', 'test.py', 'some_module.py', 'some_module_2.py']
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

今度は some_module 内の listdir() の処理だけがモックに差し変わっていることがわかります。
some_module_2 内の listdir() の処理には影響がないことが確認できます。

some_module.listdir の参照先だけがモックに差し変わるので、 some_module_2.listdir は影響を受けません。

まとめ

これまで確認した挙動から、モックに差し替えたい対象のモジュールがどのように import されているかによって patch する対象の名前空間を変える必要があるということがわかりました。
from hoge import fuga としている場合は、そのモジュール内から直接 fuga への参照を持っていることに注意しましょう。

これまで patch の挙動をちゃんと理解してなかったのですが、この記事を読んで「なるほど!」となったので備忘のためにまとめてみました。
https://nedbatchelder.com//blog/201908/why_your_mock_doesnt_work.html

※書いてみて、 patch というよりは importfrom import の挙動の違いじゃね?と思ってきましたが・・・!

patch の動きを理解して楽しくテストしよう!