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 の動作を確かめるためにまずこのようなモジュールを用意します。
import os
from os import listdir
def some_func():
print('os.listdir(): ', os.listdir('.'))
print('listdir(): ', listdir('.'))
図で書くとこんなイメージです。
そして some_func()
を実行する以下のようなテストを用意します。
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>
という文字列を返すようにモックに差し替えてみます。
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>
という文字列を返すモックに差し替えてみましょう。
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.py
と some_module_2.py
を用意します。
import os
from os import listdir
def some_func():
print('some_func', 'os.listdir(): ', os.listdir('.'))
print('some_func', 'listdir(): ', listdir('.'))
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 してみます。
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 します。
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 というよりは import
と from import
の挙動の違いじゃね?と思ってきましたが・・・!
patch の動きを理解して楽しくテストしよう!