【Python/unittest】mockの基本的な使い方

このページにはプロモーションが含まれています

このページでは、Python での単体テスト時によく利用する mock について解説を行います。

単体テストは Python の標準モジュールである unittest で行い、その中で mock を利用することを前提とした解説を行なっていきますので、unittest を利用した単体テストについてご存知ない方は事前に下記ページに目を通しておいていただくと mock の解説も理解しやすくなると思います。

Pythonで単体テストを行う方法の解説ページアイキャッチ 【Python】unittestで単体テストを行う(カバレッジの計測についても紹介)

今回紹介する mock を利用すれば、単体テストをより効率的に行うことができるようになります。

例えば、下記のようなスクリプトについて考えてみましょう!

このスクリプトでは、is_leap_year という関数を定義しており、この関数で「今年が閏年であるかどうか」の判断を行なっています。今現在の日時を datetime.now から取得し、その日時の “年” から閏年かどうかの判断を行い、閏年の場合は True を、それ以外は False を返却するようになっています。

is_leap_year
from datetime import datetime

def is_leap_year():
    # 今日の年を取得
    today = datetime.now()
    year = today.year

    if year % 4 == 0:

        if year % 100 != 0:
            # 100の倍数でなければ閏年
            return True
        else:
            if year % 400 == 0:
                # 100の倍数でも400の倍数なら閏年    
                return True
            else:
                # 100の倍数で400の倍数でないなら閏年
                return False
    else:
        # 4の倍数でなければ閏年でない

        return False

一応説明しておくと、閏年とは、基本的に 4 の倍数の年のことを言いますが、年が “400 の倍数ではない 100 の倍数” である場合は「閏年ではない」と判断されます。つまり、2400 年は閏年になりますが、2300 年は閏年ではありません。

ここで、この  is_leap_year 関数に対し、「年が 400 の倍数の場合の動作を確認する」ためのテストケースを実施したいとします。

このようなテストケースはどのようにして実現すれば良いでしょうか?

年が 400 の倍数になるまで待つというのも1つの手段になりますが、流石に気が遠くなりますね…。今は 2023 年なので、300 年以上待つ必要があります…。

こういったテストケースを実現する際に活躍するのが、今回紹介する mock になります。これを利用すれば、いますぐ上記のテストケースを実現することが可能です。

具体的には「データ属性 year2400 であるモック」を作成し、さらに「そのモックを返却するモック」を now メソッドに差し替えて実行するようにすれば、すぐに上記のテストケースを実現することが可能となります。そして、このモックの作成やモックへの差し替えは mock モジュールから提供されるクラスや関数を利用して行います。

mockの利用例

何を言ってるか分からないという方も多いと思いますが、ここから、この説明が理解できるように解説を行なっていきますので、是非ここからの説明も読んでみていただければと思います。

mock とは

まず、モックという言葉自体を知らない人もおられると思いますので、モックの一般的な意味合い、そして Python における mock の意味合いについて解説していきます。

mock:モックを利用する仕組みを提供するモジュール

“mock” とは、日本語ではそのまま「モック」、もしくは「モックアップ」を意味する単語であり、要は見かけだけ出来上がった試作品を表す言葉になります。家電量販店などには、店頭に単に見た目だけを確認するための試作品が置かれていることもあると思います。これが一般的なモックとなります。

テストでは試作品で十分な部分がある

ここで重要なことは、目的を達成するためには完成品ではなくて試作品でも十分である場合があるという点になります。上記の例であれば、お客様に製品のサイズや見た目などを把握してもらうことが目的であれば、そこだけ作った試作品を店頭に置いておけば目的は達成することができます。つまり、モックでも十分であるということになります。

一般的な意味合いのモック

これって、実は単体テストにおいても同様のことが言えるんですよね。例えば、特定の関数に対する単体テストを行いたいとします。そして、その関数は他の関数を呼び出しているとします。

単体テスト時の関数構成

この場合、そのテスト対象となる関数はしっかり作り込まれている必要はありますが、そのテスト対象の関数から呼び出される関数に関してはモックで十分である場合が多いです。テスト対象となる関数で “呼び出す関数の返却値しか見ない” 場合、テスト対象の関数の動作を確認するだけであれば、テスト対象の関数から呼び出される関数は返却値を返却するだけのモックでも十分です。

つまり、本物の関数がどれだけ複雑な処理をしたとしても、テスト対象の関数が返却値しか見ないのであれば、本物の関数と同じ返却値を返却するモックを用意し、そのモックを呼び出すようにしたとしても呼び出す側の関数の動作に変わりはありません。

呼び出すのがモックでもテスト対象の関数の動作は変化しない様子

であれば、単体テスト実施時には本物の関数と差し替える形でモックを利用してやっても良いことになります。

こんな感じで、単体テストにおけるモックも一般的なモックと同様の意味であり、要は「見かけだけ出来上がった試作品」のこととなります。ただし、この「見かけ」は “返却値” であったり、”発生する例外” であったり、”データ属性” であったりします。モックは自由に作成することができますので、見かけの部分のみを状況に応じて作成してやれば良いです。

例えば先ほどの例であれば、本物の関数が True しか返却しないのであれば、True を返却するモックを用意してテスト対象の関数の単体テストを実施すれば良いことになります。また、モックはテストケース毎に異なるものを利用することも可能です。

本物の関数と同じ返却値を返却するモック

ここまでの説明を聞くと、モック= スタブ と思う人もいるかもしれません。その方は勘が良いです!

ただ、モックにはスタブだけでなく、もっと様々な用途が存在します。例えばモックが返却するデータを別のモックとするようなこともできます。モックは関数やメソッドの代替だけでなく単なるデータとしても利用可能です。

返却値をモックとする例

また、スタブをモックで実現するにしても、スタブよりも簡単にスタブ同様の役割を持つものを用意することが可能です。

そして、こういったモックを利用するための仕組みを提供するのが mock モジュールとなります。この mockunittest から提供されるモジュールとなります。

スポンサーリンク

モックを利用する流れ

続いてモックを利用する流れについて解説しておきます。

モックを利用するためには、事前にモックを作成しておく必要があります。モック自体は、後述でも解説するように MagicMock というクラスのコンストラクタを実行することで作成することができます。ただ、単にコンストラクタを実行するだけだと作成されるモックはまっさらな状態となるため、用途に合わせてモックの設定を行う必要があります(見かけ部分を作り込む)。

さらに、モックは本物の関数やメソッドなどの「本物のオブジェクト」と一時的に差し替える形で利用します。

例えば先ほどの例では、テスト対象の関数から呼び出される関数をモックに差し替えても問題ないと説明しました。この差し替えを実現するためには、まず、その呼び出される側の関数の代わりとなるモックを作成し、必要な設定を行います。例えば返却値の設定を行い、その設定された返却値を返却するのみのモックを用意しておきます。

モックを用意する様子

さらに、テスト対象の関数から呼び出される関数を用意したモックで差し替えます。これにより、テスト対象の関数が実行された際に、その関数から用意したモックが呼び出されるようになります。そして、そのモックは事前に設定した通りに動作します。

関数をモックに差し替える様子

この、モックを作成し、そのモックの設定を行い、さらに特定のオブジェクト(関数オブジェクトなど)をそのモックに差し替える、というのがモックを利用する際の基本的な流れとなります。

特定のオブジェクトをモックに差し替える最大のメリットは、その特定のオブジェクトが呼び出された時の動作をプログラマー自身が自由自在に設定できるという点になります。

例えば、Python の標準モジュールである random には randint という関数が定義されており、この関数はランダムな整数を返却する関数になります。

そして、テスト対象の関数が randint を呼び出すような関数であり、さらに randint1 を返却した時の動作を確認したいテストケースが存在したとします。ですが、randint はランダムな整数を返却するため、1 を返却してくれるとは限らず、そのテストケースで確認したい動作が確認できない可能性があります。

本物の関数を利用すると意図したテストが行えない様子

それに対し、返却値が 1 となるモックを作成し、そのモックを randint 関数に差し替えてからテスト対象の関数を実行するようにしてやれば、必ずモックは 1 を返却してくれるので、確認したい動作を確実に実現することができます。

モックを利用することで確実に意図したテストが行えるようになる様子

こんな感じで、モックを利用すれば引数等で返却値の制御が不能な関数が呼び出されていても、その関数の返却値を自由自在にコントロールできるようになります。

また、テスト対象の関数が “まだ開発途中の関数” を呼び出すような場合にもモックは有効です。その開発途中の関数をモックに差し替えてやれば、テスト対象の関数のテストは実施可能となります。

開発中の関数をモックに差し替える例

特に複数人で関数等の開発を並行して行う場合、各関数の開発完了タイミングは異なることの方が多いです。そんな場合でも、モックを利用すれば、開発途中の関数をモックに差し替え、出来上がった関数から先に単体テストを実施するようなことも可能となります。

モックの作り方

ここまでの説明で「モックがどういったものであるか?」についてはイメージが湧くようになったのではないでしょうか?

続いてモックの使い方について解説していきます。まずは、モックの作り方について解説します。

MagicMock のコンストラクタで生成

モックは unittest.mock で定義される Mock クラスや MagicMock クラス等のコンストラクタの実行によって作成することができます。ここでは詳細な説明は避けますが、MagicMockMock のサブクラスであり、Mock + α の機能を持つクラスとなっていますので、基本的には MagicMock を利用するので良いと思います。

ということで、単にモックを作成するのであれば、下記のような処理を実装してやれば良いことになります。下記においては obj_mock がモックとなります。

モックの作成
from unittest.mock import MagicMock

obj_mock = MagicMock()

スポンサーリンク

特定の値を返却するモックの作り方

ただし、モックは単に作成するだけだとまっさらなモックで、見かけすら出来上がっていない状態です。このモックに必要な設定のみを行なって「見かけ」のみ出来上がったモックに仕立てていきます。

まず覚えておくべき設定は return_value となります。モックは呼び出し可能なオブジェクトであり、return_value を設定しておくとモックを呼び出した時に設定した return_value が返却されるようになります。

この return_value は下記のように MagicMock のコンストラクタに return_value 引数の値としてオブジェクトを指定したり、

モックの返却値の設定1
from unittest.mock import MagicMock

func_mock = MagicMock(return_value=1000)

下記のように MagicMock のインスタンスのデータ属性 func_mock.return_value にオブジェクトの指定を行なったりすることで設定することが可能です。

mockの返却値の設定2
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.return_value = 1000

そして、前述の通り、モックの呼び出しを行えば返却値として return_value に設定された値・オブジェクトが返却されるようになります。したがって、例えば下記を実行すれば、必ず print では 1000 が出力されることになります。

mockからの返却値の取得
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.return_value = 1000

ret = func_mock()
print(ret)

モックの便利なところは引数が何でも良いところになります。つまり、モックに対して引数の設定は行う必要がありません。例えば下記のように func_mock に引数を指定しても先ほどと同じ結果が得られます。

mockの引数
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.return_value = 1000

ret = func_mock(100, [200, 300], MagicMock())
print(ret)

モックを関数やメソッドとして利用する場合は、この return_value を設定して返却値のみを定義しておくことが多いです。この return_value については是非覚えておいてください。

特定の例外を発生するモックの作り方

さて、先ほどはモックに return_value を設定することで特定の値を返却するモックを作成しましたが、モックに side_effect を設定することで呼び出しされた際に特定の例外を発生させるモックを作成することも可能です。

例えば下記では、func_mock() により例外 ValueError が発生することになります。

mockでの例外の発生
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.side_effect = ValueError

try:
    ret = func_mock()
except ValueError:
    print('ValueErrorが発生')

他の関数を呼び出すモックの作り方

この side_effect に関しては使い方は様々で、先ほどのように例外を指定すれば、その例外を発生させるモックとなりますし、side_effect に関数を指定すれば、その関数を呼び出すモックとなります。この場合、モック呼び出し時に指定された引数が、side_effect に指定した関数に渡されることになります。

例えば下記の場合、func_mock を呼び出すと print 関数が実行されることになります。

モックからの他の関数の呼び出し
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.side_effect = print

ret = func_mock('aiueo')

スポンサーリンク

呼び出すたびに動作が変化するモックの作り方

また、side_effect にはイテラブルなオブジェクトを指定可能です。例えば side_effect にリストを指定すれば、そのモックを呼び出す度にリストの要素に応じてモックの動作を変化させることが可能です。

1回目にモックを呼び出した際には、モックはリストの先頭の要素に応じて動作することになります。リストの先頭の要素が例外であれば例外が発生しますし、オブジェクトであれば、そのオブジェクトが返却されることになります。同様に、2回目にモックを呼び出した際には、モックはリストの先頭から2つ目の要素に応じて動作することになります。以降も同様です。

こんな感じで、side_effect にリスト等のイテラブルなオブジェクトを指定することで、呼び出す度に動作が変化するモックを実現することが可能となります。

例えば下記であれば、1回目の func_mock の呼び出しでは例外 AttributeError が発生し、2回目では例外 ValueError が発生、3回目は例外が発生せずに 15 が返却されるようなモックを実現することができます。

呼び出す度に動作が変わるモック
from unittest.mock import MagicMock

func_mock = MagicMock()
func_mock.side_effect = [AttributeError, ValueError, 15]

try:
    ret = func_mock()
except AttributeError:
    print('AttributeError')

try:
    ret = func_mock()
except ValueError:
    print('ValueError')

ret = func_mock()
print(ret)

特定のデータ属性を持つモックの作り方

また、モックは関数やメソッドとしてだけでなく、単なるデータとしても利用することが可能です。MagicMock のインスタンスには、アクセスしたデータ属性が存在しない場合は自動的にそのデータ属性が追加されるという特徴があります。そして、その追加されるデータ属性も MagicMock のインスタンス、すなわちモックとなります。この特徴を利用すれば、モックに様々なデータ属性を持たせることができることになります。

例えば下記では、作成した時点で data_mock はデータ属性 test_a は所持していませんが、data_mock.test_a.test_b.test_c により test_a がデータ属性として追加されることになります。このデータ属性もモックとなります、さらに、その追加されたモックにはデータ属性 test_b が追加され、test_b にはデータ属性 test_c が追加されることになります。さらに、そのデータ属性 test_c200 を参照することになります。

データ属性の追加
from unittest.mock import MagicMock

data_mock = MagicMock()
data_mock.test_a.test_b.test_c = 200

print(data_mock.test_a)
print(data_mock.test_a.test_b)
print(data_mock.test_a.test_b.test_c)

つまり、上記を実行した際には data_mock は次の図のような構成となります(下図においては mockdata_mock を表しています)。

モックに新たなデータ属性としてモックが追加されていく様子

本来であれば存在しないデータ属性にアクセスすると例外が発生するのですが、モックの場合は自動的に存在しないデータ属性が追加されるようになっています。そのため、簡単に様々なデータ属性を持つモックを作成することが可能です。また、今回の場合、test_c は作成された時点ではモックとなりますが、= 200 が実行される際に test_c は整数型(を参照する)となるため、モックではなくなります。

こんな感じで、モックにはデータ属性をどんどん追加していくことが可能であり、わざわざ新たなクラスを定義しなくても特定のデータ属性を持つオブジェクトを簡単に作成することができます。

また、追加されたデータ属性は前述の通りモックであり、モックは呼び出し可能なオブジェクトとなります。従って、モックに追加されたデータ属性はデータとして利用するだけでなく、メソッドのように利用することも可能です。例えば下記のような処理では test_c を呼び出すことが可能となり、その返却値は 200 となります。

データ属性の追加
from unittest.mock import MagicMock

data_mock = MagicMock()
data_mock.test_a.test_b.test_c.return_value = 200
ret = data_mock.test_a.test_b.test_c()
print(ret)

モックへの差し替え

さて、先ほどモックの作り方について説明し、その中でモックを直接利用する(呼び出す)例を示してきました。

ただ、モックは直接利用するのではなく、他のオブジェクトと差し替える形で利用することが多いです。

次は、このモックを他のオブジェクトに差し替える手順について説明していきます。

スポンサーリンク

モックの使い方の考え方

前述の通り、モックは他のオブジェクトに差し替える形で利用することが多いです。

“他のオブジェクト” の代表例は関数オブジェクトになります。関数オブジェクトをモックに差し替えることで、その関数オブジェクトが呼び出しされた際にモックが呼び出しされるようになります。呼び出しされた際には、モックに返却値が設定されていれば、その返却値が返却されることになりますし、モックに例外が発生されるように設定されていれば例外が発生することになります。要は、モックの設定に応じた動作が実行されることになります。

差し替え対象のオブジェクトの代わりにモックを参照させる

そして、このオブジェクトとモックとの差し替えは「そのオブジェクトの代わりにモックを参照させる」ことで実現されます。

プログラム内で利用可能なオブジェクトは変数やモジュールによって必ず参照されています。その参照の参照先をモックに変更することが「モックへの差し替え」なります。

差し替えのイメージ

そして、この参照の参照先の変更は、ご存知の通り = 演算子によって実現可能です。また、後述で紹介する patch 関数によっても実現することが可能です。

この = 演算子を利用した差し替えと patch による差し替えの違いについては後述で解説しますが、まずは参照の変更のイメージがつきやすいため、= 演算子を利用した形でモックへの差し替えの解説をしていきたいと思います。

オブジェクトのモックへの差し替え例

例えば、前述の例でも少し紹介した randint 関数をモックに差し替えることを考えてみましょう!

まずは、下記のようなスクリプトについて考えてみましょう。このスクリプトの最後の print では 0100 の整数がランダムに出力されることになります。これは、randint0100 の整数をランダムに返却するためです。

randintをそのまま利用する例
import random

ret = random.randint(0, 100)
print(ret)

次は、random モジュールの randint をモックと差し替えて、最後の print で必ず 10 が出力されるようにしていきたいと思います。

この差し替えは、先ほども説明したように、まずは返却値が 10 に設定されたモックを作成し、random モジュールからの randint への参照の参照先をモックに変更することで実現することができます。そして、この参照先の変更は、= 演算子により実現可能です。

具体的には、下記のような処理となります。

randintのモックへの差し替え
from unittest.mock import MagicMock
import random

randint_mock = MagicMock(return_value=10)
random.randint = randint_mock

ret = random.randint(0, 100)
print(ret)

上記では、まず返却値が 10 に設定されたモック randint_mock を作成し、下記で random モジュールからの randint への参照を randint_mock に変更することで、randintrandint_mock との差し替えを実現しています。

差し替えの実行
random.randint = randint_mock

上記の処理を実行した後は、random.randint() 実行時には randint_mock が呼び出されることになります。これは、random.randint という参照の参照先が本物の randint 関数から randint_mock に変更されたからになります。

元々、random モジュールには本物の randint 関数の実体が存在しています。そして、その本物の関数は random モジュールから random.randint によって参照されています。ここでポイントになるのが、random.randintrandint 関数の実体そのものではなく、randint 関数への参照であるという点になります。random.randintrandint 関数の実体を参照しているため、random.randint を呼び出した際には、その参照先の randint 関数が実行されることになります。

random.randint=randint_mockの意味合い

そして、random.randint = randint_mock を実行すれば、random.randint の参照先が randint_mock に変更されることになります。参照先が変更されたため、これ以降は random.randint の呼び出しを行うと randint_mock が実行されることになります。

random.randint=randint_mockの意味合い2

そのため、random.randint = randint_mock 以降に random.randint() を実行すると必ず randint_mock の返却値である 10 が返却されることになります。重要なのは、このような参照の「参照先」の変更によってオブジェクトのモックへの差し替えが実現されるという点になります。

こんな感じで、モックを作成し、そのモックを他のオブジェクトに差し替えることで、そのオブジェクトの動作をモックの動作に置き換えることが可能となります。そして、この差し替えは、差し替え対象となるオブジェクトへの参照をモックに変更することで実現することができます。

差し替え不可なオブジェクトをモックに差し替える

ただし、オブジェクトがイミュータブルである場合、そのオブジェクトからモックを参照させる場合に例外が発生することになります。

例えば、datetime モジュールから提供される datetime クラスには now メソッドが存在します。ただし、datetime クラスはイミュータブルであるため、先ほどと同じ要領で datetime クラスからの now メソッドへの参照をモックに変更しようとすると例外が発生します。

イミュータブルなオブジェクトの差し替え
from unittest.mock import MagicMock
from datetime import datetime

value = datetime(1234, 1, 2, 3, 4, 5)
now_mock  = MagicMock(return_value=value)
datetime.now = now_mock

ret = datetime.now()
print(ret)

上記を実行すれば、datetime.now = now_mock 実行時に下記の例外が発生して差し替えに失敗します。

cannot set 'now' attribute of immutable type 'datetime.datetime'

では、このような場合はどうやってオブジェクトのモックへの差し替えを実現すれば良いでしょうか?

今回の場合は解決方法は簡単で、now を直接差し替えるのではなく、datetime クラス自体をモックに差し替えることで例外を解消することができます。前述の通り、datetime クラスはイミュータブルでありデータ属性やメソッドのモックへの差し替えは不可です。

イミュータブルなオブジェクトの差し替え1

ですが、datetime クラス自体をモックに差し替えてやれば、いくらでも変更可能となります。そして、特定のデータ属性を持つモックの作り方 で説明したように、モックにはデータ属性を簡単に追加することができ、その追加されたデータ属性もまたモックとなります。そして、モックは呼び出し可能で、return_value の設定によって返却値を指定することも可能です。

したがって、datetime クラスに差し替えたモックに now を追加し、その now の返却値を設定してやれば、datetime.now メソッド呼び出し時にモックが呼び出されるようになります。

イミュータブルなオブジェクトの差し替え2

この考え方に基づいて datetime.now メソッドのモックへの差し替えを行う例が下記のようになります。

datetime.nowの差し替えの実現
from unittest.mock import MagicMock
from datetime import datetime

value = datetime(1234, 1, 2, 3, 4, 5)

# モックを作成する
datetime_mock  = MagicMock()

# datetimeクラスをdatetime_mockに差し替え
datetime = datetime_mock

# datetime_mockにモックnowを追加して返却値をvalueに設定
datetime_mock.now.return_value = value

# datetime_mockのnowが呼び出される
ret = datetime.now()
print(ret)

このように、クラスのメソッドがモックに差し替え不可な場合でも、そのクラス自体をモックに差し替えてやれば、後は自由自在にそのモックを変更することができ、メソッドの差し替えを行うことができるようになります。

モックの差し替えと有効範囲

モックの差し替えの考え方に関しては理解していただけたでしょうか?

= 演算子を利用し、差し替え対象のオブジェクトへの参照をモックに変更することで、簡単にオブジェクトのモックへの差し替えを実現することができます。また、= 演算子を利用した差し替えは参照の参照先の変化のイメージもつきやすいと思います。ただし、実は先ほど示した = 演算子を利用したモックへの差し替えは「差し替えの有効範囲」に注意が必要になります。

= 演算子による差し替えはプログラム終了まで有効

例えば下記のようなソースコードの処理について考えてみましょう。この場合、random.randint = randint_mock を実行した後は random.randint を呼び出した際に randint_mock が呼び出しされることになります。そして、プログラムが終了するまで、ずっとその状態となります。

差し替えの有効範囲
from unittest.mock import MagicMock
import random

def print_random_1():
    ret = random.randint(0, 100)
    print(ret)

def print_random_2():
    ret = random.randint(0, 100)
    print(ret)


randint_mock  = MagicMock(return_value=1000)

random.randint = randint_mock
# これ以降ずっとrandintはrandint_mockに差し替えられたまま

print_random_1()
print_random_2()

一時的な差し替えには patch を利用する

このように、= 演算子による差し替えはプログラム終了まで有効となります(もちろんプログラムが一度終了すれば差し替えは無効になります)。したがって一度モックに差し替えた後、プログラム実行中に元のオブジェクトに戻したい場合は、別途そのための処理が必要となります。

そのため、プログラム終了するまでオブジェクトをモックに差し替えておくので問題なければ = 演算子で差し替えを行うのでも良いのですが、一時的にのみオブジェクトをモックに差し替えたい場合は別の方法で差し替えを行うことをお勧めします。

そして、その「別の方法」とは、mock から提供される patch を利用した差し替えになります。patch を利用することで、一時的にのみ差し替えを行うことが可能となります。

patch の利用:コンテキストマネージャー

この patch の使い方は大きく分けて2つあります。1つ目がコンテキストマネージャーとして利用する使い方で、2つ目がデコレーターとして利用する使い方になります。

まずは、1つ目のコンテキストマネージャーとして利用する使い方について説明していきます。

patch での with ブロック内のみのモックへの差し替え

patch をコンテキストマネージャーとして利用した場合、オブジェクトへのモックへの差し替えが with ブロック内でのみ有効となります。patch をコンテキストマネージャーとして利用する場合、下記のような形式で記述を行います。前述の通り、このように patch を利用すれば、with ブロック内のみでオブジェクトのモックへの差し替えが行われることになります。

withブロック内のみの差し替え
from unittest.mock import patch
with patch('差し替え対象のオブジェクトへの参照') as モック:
    # モックの設定

    # 差し替え対象のオブジェクトの利用

今までのモックの使い方と大きく異なるようにも思えますが、本質的な部分は同じとなります。今までは自身でモックを作成し、モックの設定を行い(返却値など)、そのモックを他のオブジェクトに差し替えることでモックを利用してきました。

上記のように patch を利用した場合、モックの作成と差し替えは patch の中で行ってくれるため、後はモックの設定のみを行えば今まで通りの考え方でモックが利用されることになります。そして、モックに差し替えられるオブジェクトは “patch の第1引数に指定した参照” の参照先のオブジェクトとなります。つまり、patch の第1引数に指定した参照の参照先がモックに変更されます。patch の第1引数は文字列で指定する必要がある点に注意してください。

patchの動作の説明図

例えば下記のようなソースコードの場合、with ブロック内でのみ randintrandint_mock に差し替えられることになります。そして、randint_mock には return_value = 1000 を設定しているため、randint を呼び出すと必ず 1000 が返却されることになります。ただし、差し替えは with ブロック内でのみ有効であるため、with ブロックを抜けて randint を呼び出すと本物の randint が呼び出され、0100 のランダムな整数が返却されることになります。

withブロック内のみrandintの差し替え
from unittest.mock import patch
import random

with patch('random.randint') as randint_mock:
    randint_mock.return_value = 1000
    ret = random.randint(0, 100)
    print(ret)

ret = random.randint(0, 100)
print(ret)

patch の引数指定でモックの設定を行う

また、patch の引数指定により作成するモックの設定を行うことも可能です。例えば下記のようなソースコードの場合、patch の中でモックの作成と差し替えだけでなく、モックの設定(return_value)まで行われることになります。この場合、with ブロック内でモックを参照する必要がなくなるため、as を利用してモックを受け取る処理も不要となります。

patchでのモックの設定
from unittest.mock import patch
import random

with patch('random.randint', return_value=1000):
    ret = random.randint(0, 100)
    print(ret)

ret = random.randint(0, 100)
print(ret)

先ほども説明しましたが、patch を利用することで今までとモックの使い方が大きく変わったようにも思えますが、patch の中で今までと同様の使い方の手順が実施されているだけであり、モックの作成とモックの設定、さらにはモックへの差し替えが必要な点は同じです。

patch の本来の目的は「差し替え」でありモック作成ではない

ただ、patch は必ずしもモックを作成するとは限らないという点に注意が必要です。patch はあくまでもオブジェクトを他のオブジェクトに差し替えることを目的とした仕組みであり、モックを作ることを目的とはしていません。必要があればモックを作成してくれますが、不必要な場合はモックを作成しません。

例えば、patchnew 引数(もしくは第2引数)を指定した場合、第1引数に指定したオブジェクトが new 引数に指定したオブジェクトと差し替えられることになります。例えば第1引数に関数オブジェクトを指定し、new 引数に他の関数を指定した場合、第1引数呼び出し時には new 引数に指定した関数が実行されることになります。

この場合、第1引数に指定したオブジェクトはモックではなく new 引数で指定されたオブジェクトに差し替えられるわけですから、わざわざ patch はモックを作成する必要はありません。必要がないので、この場合は patch でモックは作成されません。

例えば下記の場合、patchnew 引数に my_randint を指定しているため、patch ではモックを作成せず、random.randintmy_randint への差し替えが行われることになります。そして、この場合は as で得られるのは new 引数で指定したオブジェクトとなります。したがって、with ブロック内で new_funcrandint が呼び出された際には、両方とも my_randint が呼び出しされることになります。

モックが作成されない例
from unittest.mock import patch
import random

def my_randint(a, b):
    return 1050

with patch('random.randint', new=my_randint) as new_func:
    ret = new_func(0, 100)
    print(ret)

    ret = random.randint(0, 100)
    print(ret)

ret = random.randint(0, 100)
print(ret)

このように、patch は必ずしもモックを作成するものではない点に注意してください。モックを作成し、そのモックを第1引数に指定した参照の参照先のオブジェクトに差し替えたい場合は、この節の前半に紹介したような第1引数のみを指定する、もしくは第1引数+モックの設定を行うキーワード引数(return_valueside_effect)のみを指定するようにすれば良いです。

スポンサーリンク

patch の利用:デコレーター

次は、patch の2つ目の使い方となる「デコレーターとしての使い方」について解説していきます。

patch での関数内のみのモックへの差し替え

patch をデコレーターとして利用することで、そのデコレーターを適用した関数 or メソッド内でのみモックへの差し替えを有効とすることができます。

関数・メソッド内のみの差し替え
from unittest.mock import patch
@patch('差し替え対象のオブジェクトへの参照')
def 関数名(その他の引数, モック)
    # モックの設定

    # 差し替え対象のオブジェクトの利用

基本的な考え方はコンテキストマネージャーとして利用する場合と同様です。要は patch によって必要に応じてモックが作成され、”第1引数に指定した参照” の参照先が他のオブジェクトに変更されます。そして、作成されたモックが引数としてデコレーターを適用した関数やメソッドに渡されることになります。

例えば下記の場合、@patch によりモックが作成されて random.randint と差し替えられ、その作成されたモックが print_random 実行時に引数 randint_mock として print_random に渡されることになります。

そして、print_random 内で randint_mockside_effect = (1, 2, 3) を指定しているため、1回目の randint 呼び出し時の返却値は 1、2回目の randint 呼び出し時の返却値は 2 となります。@patch で差し替えを行なった場合、@patch を適用した関数やメソッド内でのみ差し替えが有効となるため、その関数やメソッド内で randint を呼び出した際に実行されるのはモックとなります。ただし、関数やメソッド外で randint を呼び出した際には、本物の randint が実行されることになります。

@patchの利用例
from unittest.mock import patch
import random

@patch('random.randint')
def print_random(min, max, randint_mock):

    randint_mock.side_effect = (1, 2, 3)

    ret = random.randint(min, max)
    print(ret)

    ret = random.randint(min, max)
    print(ret)

print_random(0, 100)

ret = random.randint(0, 100)
print(ret)

また、例は省略しますが、コンテキストマネージャーとして利用する場合と同様に、@patch の引数に side_effectreturn_value を指定することで @patch 内でモックの設定を行うことも可能です。また、@patchnew 引数を指定した場合はモックは作成されません。

patch をデコレーターとして利用する場合の注意点

デコレーターとして patch を利用する際の注意すべき点は、そのデコレーターを適用する関数やメソッドの仮引数になります。

@patch でモックが作成される場合@patch を適用する関数やメソッドは作成されたモックを受け取るための仮引数が必要となります。それに対し、@patch でモックが作成されない場合@patch を適用する関数やメソッドは作成されたモックを受け取るための仮引数は不要です。

つまり、@patch でモックが作成されるかどうかによって、@patch を適用する関数やメソッドへの “モックを受け取るための仮引数” を追加する必要性の有無が変わります。したがって、@patch でモックが作成されるかどうかを考慮して仮引数を決定する必要があります。

例えば、下記の場合、@patch でモックは作成されないため、print_random 関数にモックを受け取るための仮引数を用意しておくと実行時に例外が発生します。この際に print_random に指定される引数は print_random(0, 100) 実行時に引数で指定される実引数2つのみとなるので、mock 仮引数への指定が不足しており例外が発生します。

モックが作成されない例
from unittest.mock import patch
import random

def my_randint(a, b):
    return 1050

@patch('random.randint', new=my_randint)
def print_random(min, max, mock):

    mock.side_effect=(1, 2, 3)

    ret = random.randint(min, max)
    print(ret)

    ret = random.randint(min, max)
    print(ret)

print_random(0, 100)

ret = random.randint(0, 100)
print(ret)

このように、モックが作成されるかどうかによって必要な仮引数が変化するという点には注意が必要となります。おそらく、デコレーターとして利用する場合も、コンテキストマネージャーとして利用する場合も、patch はモックへの差し替えを目的に利用することが多いと思いますので、まずはモックが作成されることを前提に実装を進めても良いと思います。ただ、モックを利用しない使い方をする際や、仮引数が多すぎる旨を伝える例外が発生した際には、改めて patch がモックを作成するかどうかについて考えてみるのが良いと思います。

単体テストでのモックの利用例

ここまで、単体テストとは結び付けずにモックについて解説してきましたので、最後に、ここまでの復習の意味も込めて、単体テストでのモックの利用例を紹介しておきます。

ページの冒頭でも紹介した下記の is_leap_year 関数に対する単体テストを例に説明をしていきたいと思います。

main.py
from datetime import datetime

def is_leap_year():
    # 今日の年を取得
    today = datetime.now()
    year = today.year

    if year % 4 == 0:

        if year % 100 != 0:
            # 100の倍数でなければ閏年
            return True
        else:
            if year % 400 == 0:
                # 100の倍数でも400の倍数なら閏年    
                return True
            else:
                # 100の倍数で400の倍数でないなら閏年
                return False
    else:
        # 4の倍数でなければ閏年でない

        return False

ページの冒頭では、この is_leap_yeardatetime.now() で得られる日時の年が 400 の倍数である場合のテストをどうやって実施すれば良いか?という質問をしました。おそらく、ここまで読み進めていただいた方であれば、その答えはもう見つかっているはずです。そうです、モックを利用すれば良いだけです。

ということで、上記の is_leap_year をモックを利用したテストケースの実装例を示していきます。

モックを利用したテストケースの実装例

上記の is_leap_year 関数をモックを利用して実装したテストケースの例は下記のようになります。上記の is_leap_year 関数を定義しているモジュールが main (ファイルとしては main.py) であることを前提としたソースコードとなっており、is_leap_yearmain モジュールから import して実行するようになっています。

test_main.py
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
import main

class TestIsLeapYear(unittest.TestCase):

    def test_multiples_of_400(self):
        '''年が400の倍数の場合のテスト'''

        ret_now = datetime(2400, 1, 2, 3, 4, 5)
        with patch('main.datetime') as datetime_mock:
            datetime_mock.now.return_value = ret_now
            ret = main.is_leap_year()

        self.assertTrue(ret)

    @patch('main.datetime')
    def test_multiples_of_100(self, datetime_mock):
        '''年が100の倍数の場合のテスト'''

        ret_now = MagicMock(year=2300)
        datetime_mock.now.return_value = ret_now
        ret = main.is_leap_year()

        self.assertFalse(ret)

    def test_multiples_of_4(self):
        '''年が4の倍数かつ100の倍数でない場合のテスト'''

        ret_now = MagicMock(year=2304)
        with patch('main.datetime') as datetime_mock:
            datetime_mock.now.return_value = ret_now
            ret = main.is_leap_year()

        self.assertTrue(ret)

    @patch('main.datetime')
    def test_multiples_of_not_4(self, datetime_mock):
        '''年が4の倍数でない場合のテスト'''

        ret_now = MagicMock(year=2025)
        datetime_mock.now.return_value = ret_now
        ret = main.is_leap_year()

        self.assertFalse(ret)

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

test_multiples_of_400 が「年が 400 の倍数の場合の動作確認」を行うためのテストケースとなっています。一応、main.pyのカバレッジを 100% にするために他のテストケースも用意しています。下記ページで紹介している手順でカバレッジを計測すれば、main.py のカバレッジが 100% になっていることは確認できると思います。

Pythonで単体テストを行う方法の解説ページアイキャッチ 【Python】unittestで単体テストを行う(カバレッジの計測についても紹介)

やってることは、おそらく皆さんが想像している通り、モックの返却値をテストケースに従って設定し、各テストケースの中で is_leap_year で利用している now メソッドをモックに差し替えるようにしているだけです。モックの差し替えは patch で実現しており、テストケースごとに patch の利用の仕方(デコレーター or コンテキストマネージャー)は異なりますが、特にこの利用の仕方の違いに意味はないです。いろんな種類の差し替え方を示したかっただけです。

やっていることは単純ですが、それでもいくつかポイントがありますので、そのポイントについて説明しておきます。

スポンサーリンク

ポイント1:差し替え対象は datetime クラス

今回モックへの差し替えを行いたい対象のオブジェクトは datetimenow メソッドになります。

ですが、差し替え不可なオブジェクトをモックに差し替える でも説明したように、datetime はイミュータブルなクラスとなっており、datetimenow メソッドへの参照をモックに変更することができません。差し替え不可なオブジェクトをモックに差し替える では、このことを = 演算子を利用した差し替えによって確認しましたが、イミュータブルなオブジェクトが変更できないのは patch でも同様です。

そのため、now メソッドを直接差し替えるのではなく datetime 自体をモック(datetime_mock)に差し替え、そのモックに now メソッドの代わりとなるモック(datetime_mock.now)を持たせることで now メソッドの差し替えを間接的に行なっています。

今回はテスト対象となる is_leap_year では datetime の now メソッドしか利用していないため datetime_mock には now しか持たせていませんが、テスト対象の関数が datetime の他のメソッドも利用していた場合、datetime_mock にはそのメソッドの代わりとなるモックを持たせる必要があるので注意してください。

ポイント2:patch の第1引数は 'main.datetime'

また、今回の例の場合、main モジュールが利用している datetime をモックに差し替えるために patch の第1引数に指定しなければならない文字列は 'main.datetime' となります。'datetime.datetime' ではないため注意してください。今回差し替えを行いたいのは main から参照されている datetime クラスであり、datetime から参照されている datetime クラスではないため、'main.datetime' を指定する必要があります。

今回の例で参照先を変更する必要のある参照を示す図

この理由の詳細や、patch の第1引数の指定の仕方については下記ページでまとめていますので、上記のソースコードで patch の第1引数に 'main.datetime' を指定している理由を知りたい方は下記ページを読んでみてください。

patchへの第1引数の指定の仕方の解説ページアイキャッチ 【Python/unittest】patchの第1引数の指定の仕方(mock)

ポイント3:返却値にモックを利用することも可能

また、上記の TestIsLeapYear のいくつかのメソッドでは datetime_mock.now の返却値を ret_mock に設定していることが確認できると思います。ret_mockMagicMock のインスタンスであり、つまりはモックとなります。このようにモックの返却する返却値もモックとすることができます。

本来であれば、datetime.now の返却値は datetime クラスのインスタンスとなります。なので、test_multiples_of_400 で行っているように、datetime_mock.now の返却値は datetime クラスのインスタンスとしてやれば is_leap_year 関数はうまく動作させることが可能です。

ですが、is_leap_year のコードを見ていただければ分かるとおり、is_leap_yeardatetime.now から返却されるオブジェクトから利用しているのはデータ属性 year のみとなります。ですので、データ属性 year のみを持つモックを用意し、それを datetime_mock.now の返却値にしてやるのでも十分 is_leap_year のテストを実施することは可能です(もちろん is_leap_year 関数が返却されたオブジェクトの他のデータ属性も利用するのであれば、そのデータ属性も追加したモックを返却する必要があります)。

こんな感じで、モックは関数やメソッドの代わりだけでなく、単なるデータとしても利用することが可能です。さまざまな使い方ができるので、ご自身でも是非色々な使い方に挑戦してみていただければと思います。

スポンサーリンク

まとめ

このページでは、Python の mock について解説しました!

mockMagicMockpatch を提供するモジュールであり、これらによってモックの利用や関数やデータのモックへの差し替えを行えるようになります。

モックの基本的な使い方は、モックを作成し、そのモックの設定を行い、さらにそのモックを他のオブジェクトと差し替えるという流れになります。モックはプログラマー自身が好きなように設定できるため、返却値の制御が不能な関数や開発途中の関数と差し替えて利用することで、単体テストを効率的に実現することが可能となります。

単体テストを行うようになると今回紹介したような仕組みを利用したくなるケースは多いと思いますので、 mock については是非覚えておいてください!

個人的に、モックを利用する際に一番ややこしいと思ったのは patch の第1引数になります。ここの指定が上手く行えないと、オブジェクトのモックへの差し替えが上手く作用しない場合もあります。

記事内でも紹介しましたが、patch の第1引数の仕方の考え方については下記ページでまとめていますので、patch をうまく使いこなせないという方は是非読んでみていただければと思います。下記ページで解説している内容を理解していただければ、オブジェクトのモックへの差し替えを自由自在に行えるようになると思います! 

patchへの第1引数の指定の仕方の解説ページアイキャッチ 【Python/unittest】patchの第1引数の指定の仕方(mock)

同じカテゴリのページ一覧を表示