ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【python】unittestでmockを使ってテストケースを書く

 以前の記事でテストケースを書く方法を紹介しました。

 簡単なプログラムであれば上記の内容だけである程度テストはできると思いますが、他のクラスからの呼び出しだったり、対象の関数が何回呼ばれているかを確かめたり、まだ実装してないクラスを呼び出していたりという場合には対応できません。

 そういう時に便利なのがモックと言われるもので、要はダミーのクラスや関数を設定して戻り値を固定させたり呼び出し回数をカウントしたりするためのものです。

 今回はpythonに最初から含まれているunittestを使ってそんなモックを使ったテストケースを書いた時の備忘録です。


 では、始めます。


1:unittestにおけるモックの書き方
 モックの書き方を簡単にですが説明していきます。基本的には以下の部分がわかっていれば大体の場合で問題はないかと思います。

・呼び出す関数をモックにしたい場合

# -*- coding:utf-8 -*-
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() # ここの関数で_mock_functionが呼ばれる

if __name__ == '__main__':
    unittest.main()

 テストを行う関数test_hogeの前にある「@mock.patch("Hoge.function", new=_mock_function)」という記述でHogeクラスのfunction関数を自分で定義したモック関数である「_mock_function」に置き換えています。ちなみにこの書き方だとモック関数のselfはモック先のインスタンスになるので注意が必要です。

・呼び出すクラス自体をモックにしたい場合

# -*- coding:utf-8 -*-
from unittest import (TestCase, skip)
from unittest import mock

from hoge import Hoge # テスト対象ファイル読み込み

# 例えば以下のようにHogeの内部で
# 別のクラスのインスタンスを持っている場合
# class Hoge():
#     def __init__(self):
#         self.hogehoge = HogeHoge()

class TestTarget(TestCase):
    def test_hoge(self):
        hoge = Hoge()

        # hogeが持っているhogehogeインスタンスをモック用インスタンスに置き換え
        hoge.hogehoge = mock.MagicMock()

        # モックインスタンスが呼ばれているので何も行われない
        hoge.hogehoge.function()

        # モックインスタンスでfunction関数が呼ばれた回数を確認
        self.assertEqual(1, hoge.hogehoge.function.call_count)


if __name__ == '__main__':
    unittest.main()

 モックにしたいHogeHogeクラスをmock.MagicMock()というクラスで置き換えています。これはモック用のクラスで関数が呼ばれた回数を数えたりすることができる便利なクラスです。詳しい内容は参考資料に挙げているページ様や公式ドキュメントを参照してください。


2:実際にテストするコードの準備
 書き方だけ説明されてもどう使えばいいのかわからないと思うので、実際にモックを使ったテストコードを書いていきます。

 今回は以下のコードをテストする前提で進めていきます。そのまま使いたい場合はコピペして2つのファイルを同じ階層に保存してください。

・target.py

# -*- coding:utf-8 -*-
#
# 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):
        # sum1を設定
        self.sum1.setAdd(sum1_add)
        self.sum1.setLoopTimes(sum1_loop)
        # sum2を設定
        self.sum2.setAdd(sum2_add)
        self.sum2.setLoopTimes(sum2_loop)

    # main関数
    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

# -*- coding:utf-8 -*-
#
# 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」ファイルを作成します。
f:id:rikoubou:20201126172647p:plain
f:id:rikoubou:20201126172723p:plain

・test_target.py

# -*- coding:utf-8 -*-
# target.pyファイルがある階層で以下のコマンドでテストを実行する
# python -m unittest tests.test_target

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

    # SumClassのコンストラクタモック用
    def _mock_SumClass_init(self):
        self.init_count += 1

    # sumResultsToString関数のモック用
    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() # 実行

        # SumClassのコンストラクタが呼ばれた回数を確認
        self.assertEqual(2, self._mock_sum.init_count)


    # init関数のテスト
    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)


    # main関数のテスト(全てモックの場合)
    @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)


    # main関数のテスト(計算結果を見る場合)
    @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)


    # sumResultsToString関数のテスト
    def test_05_sumResultsToString_01(self):
        target = Target()

        result = target.sumResultsToString() # 実行

        # 確認
        self.assertEqual("[result] sum1:0, sum2:0", result)


    # sumResultsToString関数のテスト2
    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に慣れていないので、もうちょっと良い書き方等があればコメントしていただけると幸いです。


・参考資料