【Python】unittestの使い方 mock(モック)

2021年6月9日

この記事はPython標準のunittestの基本的な使い方についての備忘録です。

今回は、mockを使ったunittestについて記載します。

mockを使って「メソッドの戻り値を任意の値に設定する方法」「任意の例外を発生させる方法」について記載します。

開発環境

今回は以下の環境でunittestを実装していきます。

Python 3.7.7
OS:windows10
IDE:Visual Studio Code

mockでメソッドの戻り値を任意の値にする方法

テスト対象メソッドが別のメソッドを実行し、その戻り値によって結果が変わる場合、それぞれのケースについてテストが必要となります。そんな時にmockを使用すると便利です。

呼び出し先の処理が未実装の場合などもmockを使えばスタブ化することができます。

テスト対象コード

以下のようなコードをテストします。
check_numberメソッドは、return_numberメソッドを実施して、戻り値が「ゼロ」か「偶数」か「奇数」か判定し、結果に応じたメッセージを返却します。
return_numberメソッドは、「0~10」のランダムな整数を返却します。

check_numberメソッドが正しく動作するかテストします。

import random


class MockSampleClass():
    """mockを使用したテストの対象クラス"""

    def check_number(self):
        """ゼロor奇数or偶数をチェック"""

        number = self.return_number()

        if number == 0:
            return "ゼロ"
        elif number % 2 == 0:
            return '偶数'
        else:
            return '奇数'

    def return_number(self):
        """0~10のランダムな整数を返却"""

        return random.randint(0, 10)

テストコード

各テストコードの概要は以下の通りです。
test_number_check_zeroメソッド:return_numberメソッドの戻り値が「0」の場合
test_number_check_evenメソッド:return_numberメソッドの戻り値が「偶数」の場合
test_number_check_oddメソッド:return_numberメソッドの戻り値が「奇数」の場合

from unittest import TestCase
from unittest.mock import MagicMock, patch

from mock_sample import MockSampleClass


class MockSampleClassTest(TestCase):
    """MockSampleClassのテストクラス"""
    def setUp(self):
        self.sample = MockSampleClass()

    def test_number_check_zero(self):
        """ゼロを受け取った場合のテスト"""
        mock = MagicMock()
        mock.return_value = 0

        with patch('mock_sample.MockSampleClass.return_number', mock):
            self.assertEqual(self.sample.check_number(), 'ゼロ')

    def test_number_check_even(self):
        """偶数を受け取った場合のテスト"""
        mock = MagicMock()
        mock.return_value = 6

        with patch('mock_sample.MockSampleClass.return_number', mock):
            self.assertEqual(self.sample.check_number(), '偶数')

    @patch('mock_sample.MockSampleClass.return_number', MagicMock(return_value=9))
    def test_number_check_odd(self):
        """奇数を受け取った場合のテスト"""

        self.assertEqual(self.sample.check_number(), '奇数')

    def tearDown(self):
        del self.sample

mockを使用する場合は、unittest.mockをインポートする必要があります。(コードの2行目)

使い方は、まずMagicMockのインスタンスを生成し、return_valueに返却したい値を設定します。(コードの14,15行目)
次に該当のメソッドにmockを適用します。(コードの17行目)
patchの第一引数にmock化したいメソッドのパスを指定します。今回はテスト対象メソッドと同じモジュールの同じクラス内にあるため、「モジュール名.クラス名.メソッド名」と指定します。もし、mock化したいメソッドが別モジュール、別クラスにある場合は注意が必要です。
patchの第二引数には、コードの14行目でインスタンス化したmockの変数を指定します。

test_number_check_oddメソッドだけ違う書き方をしています。
ここでは、メソッドの上で@patch(~)と書くことによって、テストメソッド自体をデコレートしています。(コードの28行目)

test_number_check_zeroメソッドのような書き方をした場合、コードの17行目のwith句の中にしかmockは適用されません。そのため、with句の外でself.sample.check_number()を実行しても、mockに指定した戻り値ではなく、mock化したいメソッドを普通に実施した結果が返却されることになります。

test_number_check_oddメソッドのような書き方をした場合、その関数内では必ずmockが適用されます。

テスト結果

では実際に上記のテストコードを実行してみます。


$ python -m unittest tests.test_mock
..
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

テストが成功しました。
mockを使って下位メソッドの結果に応じた判定が正しく行われていることが確認できました。

mockで例外を発生させる方法

例外発生時の動作をテストする際、実際に例外を発生させるのが難しい場合があると思います。そんな時にもmockを使用すれば、任意の例外を発生させることができます。

テスト対象コード

以下のようなコードをテストします。

calc_somethingメソッドは、calcメソッドを実施して、その結果を返却します。
calcメソッドではZeroDivisionErrorが発生する可能性があり、ZeroDivisionErrorが発生した場合は、エラーメッセージを返却します。

calc_somethingメソッドが正しく動作するかテストします。

class MockExceptSampleClass():
    """mockを使用した例外処理のテスト対象クラス"""

    def calc_something(self):
        """何らかの計算をさせて結果を返却"""

        try:
            answer = self.calc()
        except ZeroDivisionError as e:
            # エラーメッセージを返却
            return e.args[0]

        return answer

    def calc(self):
        """何らかの計算をする。ZeroDivisionErrorが発生することもある"""

        pass

テストコード

test_calc_something_exceptメソッドとtest_calc_something_decorateメソッドはどちらも同じテストを行っています。

test_calc_something_exceptメソッドではメソッド内でmockオブジェクトを作成しています。
test_calc_something_decorateメソッドではpatchデコレータを使用しています。

from unittest import TestCase
from unittest.mock import MagicMock, patch

from mock_except_sample import MockExceptSampleClass


class MockExceptSampleClassTest(TestCase):
    """MockExceptSampleClassのテストクラス"""
    def setUp(self):
        self.sample = MockExceptSampleClass()

    def test_calc_something_except(self):
        """例外発生時のテスト"""
        mock = MagicMock()
        mock.side_effect = ZeroDivisionError('エラーだよ')

        with patch('mock_except_sample.MockExceptSampleClass.calc', mock):
            self.assertEqual(self.sample.calc_something(), 'エラーだよ')

    @patch('mock_except_sample.MockExceptSampleClass.calc', MagicMock(side_effect=ZeroDivisionError('エラーだよ')))
    def test_calc_something_decorate(self):
        """patchデコレータを使用したテスト"""

        self.assertEqual(self.sample.calc_something(), 'エラーだよ')

    def tearDown(self):
        del self.sample

例外を発生させる場合はside_effectに発生させたい例外を設定します。任意のエラーメッセージも設定可能です。(コードの15行目)
with句でmockを適用します。(コードの17行目)

patchデコレータを使用する場合は20行目のように記述します。

テスト結果

では実際に上記のテストコードを実行してみます。


$ python -m unittest tests.test_mock_except
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

テストが成功しました。
mockを使って例外発生時の動作が確認できました。

まとめ

今回はunittestでのmockの使い方について記載しました。

戻り値に任意の値を設定する場合はreturn_valueに設定し、例外を発生させたい場合はside_effectに設定します。

mockには他にも使い方があるので、使いこなせるようになりたいですね。

参考文献

実際に作業を実施した際には、以下のサイトを参考にさせていただきました。

・公式ドキュメント(version 3.7)
https://docs.python.org/ja/3.7/library/unittest.mock-examples.html


unittestの使い方については、他にもいろいろ記載しています。
よろしければ見ていってください。