このページでは、Python での単体テスト時によく利用する mock
について解説を行います。
単体テストは Python の標準モジュールである unittest
で行い、その中で mock
を利用することを前提とした解説を行なっていきますので、unittest
を利用した単体テストについてご存知ない方は事前に下記ページに目を通しておいていただくと mock
の解説も理解しやすくなると思います。
今回紹介する mock
を利用すれば、単体テストをより効率的に行うことができるようになります。
例えば、下記のようなスクリプトについて考えてみましょう!
このスクリプトでは、is_leap_year
という関数を定義しており、この関数で「今年が閏年であるかどうか」の判断を行なっています。今現在の日時を datetime.now
から取得し、その日時の “年” から閏年かどうかの判断を行い、閏年の場合は True
を、それ以外は False
を返却するようになっています。
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
になります。これを利用すれば、いますぐ上記のテストケースを実現することが可能です。
具体的には「データ属性 year
が 2400
であるモック」を作成し、さらに「そのモックを返却するモック」を now
メソッドに差し替えて実行するようにすれば、すぐに上記のテストケースを実現することが可能となります。そして、このモックの作成やモックへの差し替えは mock
モジュールから提供されるクラスや関数を利用して行います。
何を言ってるか分からないという方も多いと思いますが、ここから、この説明が理解できるように解説を行なっていきますので、是非ここからの説明も読んでみていただければと思います。
mock
とは
まず、モックという言葉自体を知らない人もおられると思いますので、モックの一般的な意味合い、そして Python における mock
の意味合いについて解説していきます。
mock
:モックを利用する仕組みを提供するモジュール
“mock” とは、日本語ではそのまま「モック」、もしくは「モックアップ」を意味する単語であり、要は見かけだけ出来上がった試作品を表す言葉になります。家電量販店などには、店頭に単に見た目だけを確認するための試作品が置かれていることもあると思います。これが一般的なモックとなります。
テストでは試作品で十分な部分がある
ここで重要なことは、目的を達成するためには完成品ではなくて試作品でも十分である場合があるという点になります。上記の例であれば、お客様に製品のサイズや見た目などを把握してもらうことが目的であれば、そこだけ作った試作品を店頭に置いておけば目的は達成することができます。つまり、モックでも十分であるということになります。
これって、実は単体テストにおいても同様のことが言えるんですよね。例えば、特定の関数に対する単体テストを行いたいとします。そして、その関数は他の関数を呼び出しているとします。
この場合、そのテスト対象となる関数はしっかり作り込まれている必要はありますが、そのテスト対象の関数から呼び出される関数に関してはモックで十分である場合が多いです。テスト対象となる関数で “呼び出す関数の返却値しか見ない” 場合、テスト対象の関数の動作を確認するだけであれば、テスト対象の関数から呼び出される関数は返却値を返却するだけのモックでも十分です。
つまり、本物の関数がどれだけ複雑な処理をしたとしても、テスト対象の関数が返却値しか見ないのであれば、本物の関数と同じ返却値を返却するモックを用意し、そのモックを呼び出すようにしたとしても呼び出す側の関数の動作に変わりはありません。
であれば、単体テスト実施時には本物の関数と差し替える形でモックを利用してやっても良いことになります。
こんな感じで、単体テストにおけるモックも一般的なモックと同様の意味であり、要は「見かけだけ出来上がった試作品」のこととなります。ただし、この「見かけ」は “返却値” であったり、”発生する例外” であったり、”データ属性” であったりします。モックは自由に作成することができますので、見かけの部分のみを状況に応じて作成してやれば良いです。
例えば先ほどの例であれば、本物の関数が True
しか返却しないのであれば、True
を返却するモックを用意してテスト対象の関数の単体テストを実施すれば良いことになります。また、モックはテストケース毎に異なるものを利用することも可能です。
ここまでの説明を聞くと、モック= スタブ
と思う人もいるかもしれません。その方は勘が良いです!
ただ、モックにはスタブだけでなく、もっと様々な用途が存在します。例えばモックが返却するデータを別のモックとするようなこともできます。モックは関数やメソッドの代替だけでなく単なるデータとしても利用可能です。
また、スタブをモックで実現するにしても、スタブよりも簡単にスタブ同様の役割を持つものを用意することが可能です。
そして、こういったモックを利用するための仕組みを提供するのが mock
モジュールとなります。この mock
は unittest
から提供されるモジュールとなります。
スポンサーリンク
モックを利用する流れ
続いてモックを利用する流れについて解説しておきます。
モックを利用するためには、事前にモックを作成しておく必要があります。モック自体は、後述でも解説するように MagicMock
というクラスのコンストラクタを実行することで作成することができます。ただ、単にコンストラクタを実行するだけだと作成されるモックはまっさらな状態となるため、用途に合わせてモックの設定を行う必要があります(見かけ部分を作り込む)。
さらに、モックは本物の関数やメソッドなどの「本物のオブジェクト」と一時的に差し替える形で利用します。
例えば先ほどの例では、テスト対象の関数から呼び出される関数をモックに差し替えても問題ないと説明しました。この差し替えを実現するためには、まず、その呼び出される側の関数の代わりとなるモックを作成し、必要な設定を行います。例えば返却値の設定を行い、その設定された返却値を返却するのみのモックを用意しておきます。
さらに、テスト対象の関数から呼び出される関数を用意したモックで差し替えます。これにより、テスト対象の関数が実行された際に、その関数から用意したモックが呼び出されるようになります。そして、そのモックは事前に設定した通りに動作します。
この、モックを作成し、そのモックの設定を行い、さらに特定のオブジェクト(関数オブジェクトなど)をそのモックに差し替える、というのがモックを利用する際の基本的な流れとなります。
特定のオブジェクトをモックに差し替える最大のメリットは、その特定のオブジェクトが呼び出された時の動作をプログラマー自身が自由自在に設定できるという点になります。
例えば、Python の標準モジュールである random
には randint
という関数が定義されており、この関数はランダムな整数を返却する関数になります。
そして、テスト対象の関数が randint
を呼び出すような関数であり、さらに randint
が 1
を返却した時の動作を確認したいテストケースが存在したとします。ですが、randint
はランダムな整数を返却するため、1
を返却してくれるとは限らず、そのテストケースで確認したい動作が確認できない可能性があります。
それに対し、返却値が 1
となるモックを作成し、そのモックを randint
関数に差し替えてからテスト対象の関数を実行するようにしてやれば、必ずモックは 1
を返却してくれるので、確認したい動作を確実に実現することができます。
こんな感じで、モックを利用すれば引数等で返却値の制御が不能な関数が呼び出されていても、その関数の返却値を自由自在にコントロールできるようになります。
また、テスト対象の関数が “まだ開発途中の関数” を呼び出すような場合にもモックは有効です。その開発途中の関数をモックに差し替えてやれば、テスト対象の関数のテストは実施可能となります。
特に複数人で関数等の開発を並行して行う場合、各関数の開発完了タイミングは異なることの方が多いです。そんな場合でも、モックを利用すれば、開発途中の関数をモックに差し替え、出来上がった関数から先に単体テストを実施するようなことも可能となります。
モックの作り方
ここまでの説明で「モックがどういったものであるか?」についてはイメージが湧くようになったのではないでしょうか?
続いてモックの使い方について解説していきます。まずは、モックの作り方について解説します。
MagicMock
のコンストラクタで生成
モックは unittest.mock
で定義される Mock
クラスや MagicMock
クラス等のコンストラクタの実行によって作成することができます。ここでは詳細な説明は避けますが、MagicMock
は Mock
のサブクラスであり、Mock + α
の機能を持つクラスとなっていますので、基本的には MagicMock
を利用するので良いと思います。
ということで、単にモックを作成するのであれば、下記のような処理を実装してやれば良いことになります。下記においては obj_mock
がモックとなります。
from unittest.mock import MagicMock
obj_mock = MagicMock()
スポンサーリンク
特定の値を返却するモックの作り方
ただし、モックは単に作成するだけだとまっさらなモックで、見かけすら出来上がっていない状態です。このモックに必要な設定のみを行なって「見かけ」のみ出来上がったモックに仕立てていきます。
まず覚えておくべき設定は return_value
となります。モックは呼び出し可能なオブジェクトであり、return_value
を設定しておくとモックを呼び出した時に設定した return_value
が返却されるようになります。
この return_value
は下記のように MagicMock
のコンストラクタに return_value
引数の値としてオブジェクトを指定したり、
from unittest.mock import MagicMock
func_mock = MagicMock(return_value=1000)
下記のように MagicMock
のインスタンスのデータ属性 func_mock.return_value
にオブジェクトの指定を行なったりすることで設定することが可能です。
from unittest.mock import MagicMock
func_mock = MagicMock()
func_mock.return_value = 1000
そして、前述の通り、モックの呼び出しを行えば返却値として return_value
に設定された値・オブジェクトが返却されるようになります。したがって、例えば下記を実行すれば、必ず print
では 1000
が出力されることになります。
from unittest.mock import MagicMock
func_mock = MagicMock()
func_mock.return_value = 1000
ret = func_mock()
print(ret)
モックの便利なところは引数が何でも良いところになります。つまり、モックに対して引数の設定は行う必要がありません。例えば下記のように func_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
が発生することになります。
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_c
が 200
を参照することになります。
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
は次の図のような構成となります(下図においては mock
が data_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
では 0
〜 100
の整数がランダムに出力されることになります。これは、randint
が 0
〜 100
の整数をランダムに返却するためです。
import random
ret = random.randint(0, 100)
print(ret)
次は、random
モジュールの randint
をモックと差し替えて、最後の print
で必ず 10
が出力されるようにしていきたいと思います。
この差し替えは、先ほども説明したように、まずは返却値が 10
に設定されたモックを作成し、random
モジュールからの 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
に変更することで、randint
と randint_mock
との差し替えを実現しています。
random.randint = randint_mock
上記の処理を実行した後は、random.randint()
実行時には randint_mock
が呼び出されることになります。これは、random.randint
という参照の参照先が本物の randint
関数から randint_mock
に変更されたからになります。
元々、random
モジュールには本物の randint
関数の実体が存在しています。そして、その本物の関数は random
モジュールから random.randint
によって参照されています。ここでポイントになるのが、random.randint
は randint
関数の実体そのものではなく、randint
関数への参照であるという点になります。random.randint
が randint
関数の実体を参照しているため、random.randint
を呼び出した際には、その参照先の randint
関数が実行されることになります。
そして、random.randint = randint_mock
を実行すれば、random.randint
の参照先が randint_mock
に変更されることになります。参照先が変更されたため、これ以降は random.randint
の呼び出しを行うと randint_mock
が実行されることになります。
そのため、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
クラスはイミュータブルでありデータ属性やメソッドのモックへの差し替えは不可です。
ですが、datetime
クラス自体をモックに差し替えてやれば、いくらでも変更可能となります。そして、特定のデータ属性を持つモックの作り方 で説明したように、モックにはデータ属性を簡単に追加することができ、その追加されたデータ属性もまたモックとなります。そして、モックは呼び出し可能で、return_value
の設定によって返却値を指定することも可能です。
したがって、datetime
クラスに差し替えたモックに now
を追加し、その now
の返却値を設定してやれば、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
ブロック内のみでオブジェクトのモックへの差し替えが行われることになります。
from unittest.mock import patch
with patch('差し替え対象のオブジェクトへの参照') as モック:
# モックの設定
# 差し替え対象のオブジェクトの利用
今までのモックの使い方と大きく異なるようにも思えますが、本質的な部分は同じとなります。今までは自身でモックを作成し、モックの設定を行い(返却値など)、そのモックを他のオブジェクトに差し替えることでモックを利用してきました。
上記のように patch
を利用した場合、モックの作成と差し替えは patch
の中で行ってくれるため、後はモックの設定のみを行えば今まで通りの考え方でモックが利用されることになります。そして、モックに差し替えられるオブジェクトは “patch
の第1引数に指定した参照” の参照先のオブジェクトとなります。つまり、patch
の第1引数に指定した参照の参照先がモックに変更されます。patch
の第1引数は文字列で指定する必要がある点に注意してください。
例えば下記のようなソースコードの場合、with
ブロック内でのみ randint
が randint_mock
に差し替えられることになります。そして、randint_mock
には return_value = 1000
を設定しているため、randint
を呼び出すと必ず 1000
が返却されることになります。ただし、差し替えは with
ブロック内でのみ有効であるため、with
ブロックを抜けて randint
を呼び出すと本物の randint
が呼び出され、0
〜 100
のランダムな整数が返却されることになります。
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
を利用してモックを受け取る処理も不要となります。
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
はあくまでもオブジェクトを他のオブジェクトに差し替えることを目的とした仕組みであり、モックを作ることを目的とはしていません。必要があればモックを作成してくれますが、不必要な場合はモックを作成しません。
例えば、patch
に new
引数(もしくは第2引数)を指定した場合、第1引数に指定したオブジェクトが new
引数に指定したオブジェクトと差し替えられることになります。例えば第1引数に関数オブジェクトを指定し、new
引数に他の関数を指定した場合、第1引数呼び出し時には new
引数に指定した関数が実行されることになります。
この場合、第1引数に指定したオブジェクトはモックではなく new
引数で指定されたオブジェクトに差し替えられるわけですから、わざわざ patch
はモックを作成する必要はありません。必要がないので、この場合は patch
でモックは作成されません。
例えば下記の場合、patch
の new
引数に my_randint
を指定しているため、patch
ではモックを作成せず、random.randint
の my_randint
への差し替えが行われることになります。そして、この場合は as
で得られるのは new
引数で指定したオブジェクトとなります。したがって、with
ブロック内で new_func
と randint
が呼び出された際には、両方とも 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_value
や side_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_mock
に side_effect = (1, 2, 3)
を指定しているため、1回目の randint
呼び出し時の返却値は 1
、2回目の randint
呼び出し時の返却値は 2
となります。@patch
で差し替えを行なった場合、@patch
を適用した関数やメソッド内でのみ差し替えが有効となるため、その関数やメソッド内で randint
を呼び出した際に実行されるのはモックとなります。ただし、関数やメソッド外で randint
を呼び出した際には、本物の randint
が実行されることになります。
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_effect
や return_value
を指定することで @patch
内でモックの設定を行うことも可能です。また、@patch
で new
引数を指定した場合はモックは作成されません。
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
関数に対する単体テストを例に説明をしていきたいと思います。
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_year
で datetime.now()
で得られる日時の年が 400
の倍数である場合のテストをどうやって実施すれば良いか?という質問をしました。おそらく、ここまで読み進めていただいた方であれば、その答えはもう見つかっているはずです。そうです、モックを利用すれば良いだけです。
ということで、上記の is_leap_year
をモックを利用したテストケースの実装例を示していきます。
モックを利用したテストケースの実装例
上記の is_leap_year
関数をモックを利用して実装したテストケースの例は下記のようになります。上記の is_leap_year
関数を定義しているモジュールが main
(ファイルとしては main.py
) であることを前提としたソースコードとなっており、is_leap_year
は main
モジュールから import
して実行するようになっています。
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%
になっていることは確認できると思います。
やってることは、おそらく皆さんが想像している通り、モックの返却値をテストケースに従って設定し、各テストケースの中で is_leap_year
で利用している now
メソッドをモックに差し替えるようにしているだけです。モックの差し替えは patch
で実現しており、テストケースごとに patch
の利用の仕方(デコレーター or コンテキストマネージャー)は異なりますが、特にこの利用の仕方の違いに意味はないです。いろんな種類の差し替え方を示したかっただけです。
やっていることは単純ですが、それでもいくつかポイントがありますので、そのポイントについて説明しておきます。
スポンサーリンク
ポイント1:差し替え対象は datetime
クラス
今回モックへの差し替えを行いたい対象のオブジェクトは datetime
の now
メソッドになります。
ですが、差し替え不可なオブジェクトをモックに差し替える でも説明したように、datetime
はイミュータブルなクラスとなっており、datetime
の now
メソッドへの参照をモックに変更することができません。差し替え不可なオブジェクトをモックに差し替える では、このことを =
演算子を利用した差し替えによって確認しましたが、イミュータブルなオブジェクトが変更できないのは 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'
を指定している理由を知りたい方は下記ページを読んでみてください。
ポイント3:返却値にモックを利用することも可能
また、上記の TestIsLeapYear
のいくつかのメソッドでは datetime_mock.now
の返却値を ret_mock
に設定していることが確認できると思います。ret_mock
は MagicMock
のインスタンスであり、つまりはモックとなります。このようにモックの返却する返却値もモックとすることができます。
本来であれば、datetime.now
の返却値は datetime
クラスのインスタンスとなります。なので、test_multiples_of_400
で行っているように、datetime_mock.now
の返却値は datetime
クラスのインスタンスとしてやれば is_leap_year
関数はうまく動作させることが可能です。
ですが、is_leap_year
のコードを見ていただければ分かるとおり、is_leap_year
が datetime.now
から返却されるオブジェクトから利用しているのはデータ属性 year
のみとなります。ですので、データ属性 year
のみを持つモックを用意し、それを datetime_mock.now
の返却値にしてやるのでも十分 is_leap_year
のテストを実施することは可能です(もちろん is_leap_year
関数が返却されたオブジェクトの他のデータ属性も利用するのであれば、そのデータ属性も追加したモックを返却する必要があります)。
こんな感じで、モックは関数やメソッドの代わりだけでなく、単なるデータとしても利用することが可能です。さまざまな使い方ができるので、ご自身でも是非色々な使い方に挑戦してみていただければと思います。
スポンサーリンク
まとめ
このページでは、Python の mock
について解説しました!
mock
は MagicMock
や patch
を提供するモジュールであり、これらによってモックの利用や関数やデータのモックへの差し替えを行えるようになります。
モックの基本的な使い方は、モックを作成し、そのモックの設定を行い、さらにそのモックを他のオブジェクトと差し替えるという流れになります。モックはプログラマー自身が好きなように設定できるため、返却値の制御が不能な関数や開発途中の関数と差し替えて利用することで、単体テストを効率的に実現することが可能となります。
単体テストを行うようになると今回紹介したような仕組みを利用したくなるケースは多いと思いますので、 mock
については是非覚えておいてください!
個人的に、モックを利用する際に一番ややこしいと思ったのは patch
の第1引数になります。ここの指定が上手く行えないと、オブジェクトのモックへの差し替えが上手く作用しない場合もあります。
記事内でも紹介しましたが、patch
の第1引数の仕方の考え方については下記ページでまとめていますので、patch
をうまく使いこなせないという方は是非読んでみていただければと思います。下記ページで解説している内容を理解していただければ、オブジェクトのモックへの差し替えを自由自在に行えるようになると思います!