以前の記事でテストケースを書く方法を紹介しました。
簡単なプログラムであれば上記の内容だけである程度テストはできると思いますが、他のクラスからの呼び出しだったり、対象の関数が何回呼ばれているかを確かめたり、まだ実装してないクラスを呼び出していたりという場合には対応できません。
そういう時に便利なのがモックと言われるもので、要はダミーのクラスや関数を設定して戻り値を固定させたり呼び出し回数をカウントしたりするためのものです。
今回はpythonに最初から含まれているunittestを使ってそんなモックを使ったテストケースを書いた時の備忘録です。
では、始めます。
1:unittestにおけるモックの書き方
モックの書き方を簡単にですが説明していきます。基本的には以下の部分がわかっていれば大体の場合で問題はないかと思います。
・呼び出す関数をモックにしたい場合
from unittest import (TestCase, skip)
from unittest import mock
from hoge import Hoge
class TestTarget(TestCase):
def _mock_function(self):
@mock.patch("Hoge.function", new=_mock_function)
def test_hoge(self):
hoge = Hoge()
hoge.function()
if __name__ == '__main__':
unittest.main()
テストを行う関数test_hogeの前にある「@mock.patch("Hoge.function", new=_mock_function)」という記述でHogeクラスのfunction関数を自分で定義したモック関数である「_mock_function」に置き換えています。ちなみにこの書き方だとモック関数のselfはモック先のインスタンスになるので注意が必要です。
・呼び出すクラス自体をモックにしたい場合
from unittest import (TestCase, skip)
from unittest import mock
from hoge import Hoge
class TestTarget(TestCase):
def test_hoge(self):
hoge = Hoge()
hoge.hogehoge = mock.MagicMock()
hoge.hogehoge.function()
self.assertEqual(1, hoge.hogehoge.function.call_count)
if __name__ == '__main__':
unittest.main()
モックにしたいHogeHogeクラスをmock.MagicMock()というクラスで置き換えています。これはモック用のクラスで関数が呼ばれた回数を数えたりすることができる便利なクラスです。詳しい内容は参考資料に挙げているページ様や公式ドキュメントを参照してください。
2:実際にテストするコードの準備
書き方だけ説明されてもどう使えばいいのかわからないと思うので、実際にモックを使ったテストコードを書いていきます。
今回は以下のコードをテストする前提で進めていきます。そのまま使いたい場合はコピペして2つのファイルを同じ階層に保存してください。
・target.py
from sumClass import SumClass
class Target():
def __init__(self):
self.sum1 = SumClass()
self.sum2 = SumClass()
def init(self, sum1_add, sum1_loop, sum2_add, sum2_loop):
self.sum1.setAdd(sum1_add)
self.sum1.setLoopTimes(sum1_loop)
self.sum2.setAdd(sum2_add)
self.sum2.setLoopTimes(sum2_loop)
def main(self):
self.sum1.sumLoop()
self.sum2.sumLoop()
result_str = self.sumResultsToString()
print(result_str)
def sumResultsToString(self):
result_str = "[result] sum1:{}, sum2:{}"
return result_str.format(int(self.sum1.result), int(self.sum2.result))
if __name__ == "__main__":
target = Target()
target.init(3, 5, 2, 10)
target.main()
・sumClass.py
class SumClass():
def __init__(self):
self.result = 0
self.add = 0
self.loop_times = 0
def setAdd(self, add):
self.add = add
def setLoopTimes(self, loopTimes):
self.loop_times = loopTimes
def sumLoop(self):
count = 1
while count <= self.loop_times:
self.result = self.mySum(self.result, self.add)
count += 1
def mySum(self, add1, add2):
return add1 + add2
今回はこの2つのファイルのうちの「target.py」のテストコードを書いていきます。
3:テストコードの実装
2つのファイルと同じ階層に「tests」フォルダを作成し、その中に「test_target.py」ファイルを作成します。


・test_target.py
from unittest import (TestCase, skip)
from unittest import mock
from sumClass import SumClass
from target import Target
class MockSumClass(mock.MagicMock):
init_count = 0
result_count = 0
def _mock_SumClass_init(self):
self.init_count += 1
def _mock_sumResultsToString(self):
self.result_count += 1
class TestTarget(TestCase):
_mock_sum = MockSumClass()
@mock.patch("sumClass.SumClass.__init__", new=_mock_sum._mock_SumClass_init)
def test_01__init__(self):
Target()
self.assertEqual(2, self._mock_sum.init_count)
def test_02_init(self):
target = Target()
target.sum1 = mock.MagicMock()
target.sum2 = mock.MagicMock()
target.init(1, 2, 3, 4)
self.assertEqual(1, target.sum1.setAdd.call_count)
self.assertEqual(1, target.sum1.setLoopTimes.call_count)
self.assertEqual(1, target.sum2.setAdd.call_count)
self.assertEqual(1, target.sum2.setLoopTimes.call_count)
@mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
def test_03_main_01(self):
self._mock_sum.result_count = 0
target = Target()
target.sum1 = mock.MagicMock()
target.sum2 = mock.MagicMock()
target.main()
self.assertEqual(1, target.sum1.sumLoop.call_count)
self.assertEqual(1, target.sum2.sumLoop.call_count)
self.assertEqual(1, self._mock_sum.result_count)
@mock.patch("target.Target.sumResultsToString", new=_mock_sum._mock_sumResultsToString)
def test_04_main_02(self):
self._mock_sum.result_count = 0
target = Target()
target.init(1, 2, 3, 4)
target.main()
self.assertEqual(2, target.sum1.result)
self.assertEqual(12, target.sum2.result)
self.assertEqual(1, self._mock_sum.result_count)
def test_05_sumResultsToString_01(self):
target = Target()
result = target.sumResultsToString()
self.assertEqual("[result] sum1:0, sum2:0", result)
def test_06_sumResultsToString_02(self):
target = Target()
target.init(1, 2, 3, 4)
target.main()
result = target.sumResultsToString()
self.assertEqual("[result] sum1:2, sum2:12", result)
if __name__ == '__main__':
unittest.main()
テストコードの実行は「target.py」ファイルと同じ階層で以下のコマンドで実行できます。
$ python -m unittest tests.test_target
..None
.None
..[result] sum1:2, sum2:12
.
----------------------------------------------------------------------
Ran 6 tests in 0.006s
OK
あまり良いテストコードではない気がしますが、それぞれ呼び出し回数やモック関数の呼び出しがちゃんとできています。
また、個人的にはモック用のクラスを作成してそれを呼び出すという形にした方が呼び出し回数など色々制御しやすいので楽なのではないかと思いそういう形にしています。
以上がunittestでmockを使う方法です。
正直まだ自分自身もmockに慣れていないので、もうちょっと良い書き方等があればコメントしていただけると幸いです。
・参考資料