【Python/unittest】patchの第1引数の指定の仕方(mock)

patchへの第1引数の指定の仕方の解説ページアイキャッチ

このページでは、Python の patch への第1引数の仕方についての解説を行います。

patchunittest.mock モジュールから提供される関数の1つで、主にモックと他のオブジェクトとの差し替えを行う目的で利用する関数になります。mockpatch に関しては下記ページで紹介していますので、まだご存知ない方は事前に下記ページを読んでみていただければと思います。

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

モックを利用することで Python での単体テストを効率的に実施することが可能となります。ただし、このモックを上手く利用するためには patch を使いこなす必要があります。ですが、この patch の第1引数の指定の仕方の考え方が少し難しいため、それに伴い patch を使いこなす難易度も高くなっています。

このページでは、誰でも patch やモックを使いこなせるようになるように、patch の第1引数の指定の仕方について解説していきたいと思います。

patch 関数への第1引数の仕方

最初に結論を述べておくと、patch への第1引数は下記のような形式で指定を行えば良いです。他の形式で指定しても意図した通りに patch 関数を動作させることができる場合もありますが、下記の形式で指定するのが一番シンプルで分かりやすいと思います。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

特に テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」 の部分の意味が分からないと言う方もおられるかもしれませんが、ここからの解説も読み進めていただければ理解できると思います。

具体例を示しておくと、単体テストでのテスト対象モジュールが下記のような main であり、この main から呼び出している randint をモックに差し替えたい場合、

main.py
import random

def roll_dice():
    ret = random.randint(1, 6)
    return ret

テスト対象モジュールmain となり、さらに テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」random.randint となります。したがって、patch 関数によって main の呼び出す randint をモックに差し替えたい場合は patch 関数の第1引数に 'main.random.randint' を指定すれば良いことになります。

main.py
import unittest
from unittest.mock import patch
import main

with patch('main.random.randint') as randint_mock:
    略

上記では patch をコンテキストマネージャーとして利用していますが、デコレーターとして利用する場合も同じ考え方で patch 関数の第1引数への指定を行えば良いです。

また、patch の第1引数に指定する文字列は必ず . を含む必要があります。が、上記の考え方で指定を行えば、その点も自然とクリアされることになります。

ということで、patch の第1引数には上記の形式の文字列を指定すれば良いことになります。ここからは、patch の第1引数に上記の形式の文字列を指定すれば良い理由を理解していただくために、patch の第1引数を指定する際の注意点や、patch の第1引数に指定する文字列の考え方について解説していきます。

patch への第1引数の指定時の注意点

これは個人的な意見ですが、mockpatch を利用する上で一番難しい点は、このページの本題としている patch への第1引数の指定の仕方だと思っています。公式の mock の説明ページを見ても、言いたいことはなんとなく分かるけど結局どう指定すれば良いのかが上手く飲み込めませんでした。この難しさを感じたことが、この記事を作成したキッカケとなります。

具体的に難しいと感じたのは「モジュールの import の指定の仕方によって patch への第1引数の指定の仕方も変える必要がある」という点になります。ということで、patch を利用する際には、この注意点に気をつけながら第1引数を指定する必要があります。ただ、最初に紹介した形式であれば、自然とこの注意点を考慮した上で 第1引数の指定が行われることになります。

ここからは、まずは具体例で「モジュールの import の指定の仕方によって patch への第1引数の指定の仕方も変える必要がある」という点について確認していきたいと思います。

スポンサーリンク

patch が上手く作用する例

ここでは、main モジュールを (main.py) をテスト対象モジュールとし、さらに test_main モジュール(test_main.py)から main を import して main で定義される関数のテストを行う構成を前提に説明をしていきたいと思います。また、このページの主題となる patchtest_main から実行するものとして解説していきます。

例として紹介するモジュールの構成

まずは、main のコードが下記である場合に、この main で利用されている randint を test_main.py からモックで差し替えてテストを行うことを考えていきます。

mainモジュール
import random

def roll_dice():
    ret = random.randint(1, 6)
    return ret

この場合、test_mainmain.roll_dice を実行する前に、下記のように patch の第1引数に 'random.randint' を指定して patch を実行すれば、意図通り main から呼び出しされる randint がモックに差し替えられることになります。

randintの差し替え
from unittest.mock import patch
import main

with patch('random.randint') as randint_mock:
    randint_mock.return_value = 1
    ret = main.roll_dice()

ちなみに、上記の patch 実行時に第1引数によって特定されるオブジェクトが “モックに” 差し替えられるのは、patch の引数に new 等の特別な引数を指定しないからになります。この辺りは下記ページで解説していますので、詳しくは下記ページをご参照ください。

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

patch が上手く作用しない例

ですが、下記のように main での import の仕方を変更した場合、先ほどと同様に test_main で patch の第1引数に 'random.randint' を指定すると、main から呼び出しされる randint のモックへの差し替えが行われません。つまり、mainrandint を呼び出すと、モックではなく本物の randint が実行されることになります。

importの仕方の変更
from random import randint

def roll_dice():
    ret = randint(1, 6)
    return ret

このように、テスト対象のモジュールで他のモジュールを import する際の import の仕方を変更すると、今まで上手く patch が作用していたものが上手く作用しなくなります。これを解決するためには、patch の第1引数を変更する必要があります。

では、どういう考え方に基づいて、どう変更すれば良いのか?そこが私にはよく分かっていなかったのですが、結論としては前述でも述べた下記のように指定を行えば良いと考えています。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

今回は、テスト対象モジュールmain となります。では テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」 は何になるでしょうか?結論だけ示すと  randint となります。ということで、先程の例では patch 関数の引数には 'random.randint' ではなく 'main.randint' を指定すればよかったことになります。

でも、なぜこの形式で指定すれば上手く patch  が作用することになるのでしょうか?というか、そもそもなぜ先ほどの例では上手く patch が作用しなかったのでしょうか?

この辺りを理解していただくために、次は patch 関数の第1引数の指定の考え方について整理していきたいと思います。

patch への第1引数の指定の仕方の考え方

まずは、そもそも「差し替えとは何なのか?」という点を考えていきたいと思います。これを理解することで、patch の第1引数への指定の仕方も理解できるようになると思います。 

スポンサーリンク

差し替えとは「オブジェクトへの参照」を変更すること

結論を言うと、オブジェクトのモックへの差し替えとは “そのオブジェクトへの参照の参照先をモックに変更すること” となります。先程の例のように randint を差し替えるのであれば、randint への参照の参照先をモックに変更することになります。

つまり、差し替え対象のオブジェクトを他のオブジェクトで上書きして差し替えを行うのではなく、差し替え対象への参照の参照先を他のオブジェクトに変更することで差し替えが実現されることになります。

オブジェクトの差し替えの説明図

そして、これを一時的に行うのが patch であり、patch の第1引数には “差し替えたいオブジェクトへの参照” を指定します。

ここで大事になるのが、patch の第1引数に指定する参照が “テスト対象モジュールの持つ参照であること” になります。オブジェクトに対する参照は複数存在する場合があります。そして、patch の第1引数に指定する参照はテスト対象モジュールの持つ参照でなければ、無関係な参照の参照先がモックに変更されることになります。

この辺りを意識しながら、ここからいくつか参照の参照先の変更を行う例を示していきたいと思います。ここで紹介する例では全て、先ほどと同様にテスト対象モジュールを main とし、さらに test_mainmainimport し、test_main から patch を実行した上で、main で定義される関数をテストしようとしているものとして解説していきます。

patch による参照の変更例1

まずは、下記のような main モジュールについて考えてみましょう!

前述の通り、この main は test_main から import されるものであるとします。そして、この main が呼び出す randint をモックに差し替えることを考えていきます。

mainモジュール
from random import randint

def roll_dice():
    ret = randint(1, 6)
    return ret

import とは、import 先のモジュールやクラス・関数などが利用できるよう、それらの参照を import 元のモジュールに追加することであると考えられます。

上記の場合であれば、まず test_main は mainimport することで、main モジュールを main という名前の「参照」で参照することになります。さらに、main は from import によって random モジュールから randint 関数を import することになり、この import によって main から randint 関数が参照されることになります。そして、その参照は test_main から見ると main.randint となります。

各importによって追加される参照を示す図

このような参照関係にあるため、test_main は下記を実行することで randint を呼び出すことができます。

他モジュールからのmain.randintの実行
import main

print(main.randint(1, 6))

前述の通り、オブジェクトのモックへの差し替えとは「そのオブジェクトへの参照の参照先をモックに変更すること」となります。そして、randintmain から参照されており、test_main から見れば、その参照は main.randint となります。

さらに、これも前述の通り、patch の第1引数には “差し替えたいオブジェクトへの参照” を指定します。したがって、main から呼び出しされる randint をモックに差し替えるためには、test_main から実行する patch 関数の第1引数に 'main.randint' を指定してやれば良いことになります。

例えば、下記のような test_main で patch を実行するようにすれば、

randintの差し替え(成功例)
from unittest.mock import patch
import main

with patch('main.randint') as randint_mock:
    randint_mock.return_value = 1
    ret = main.roll_dice()

下の図のように main.randint の参照先がモックに変更され、main から randint を呼び出した際にはモックが実行されることになります(モックは patch の中で自動的に作成されます)。要は、patch とは、一時的に第1引数で指定された参照の参照先(矢印の先端)を他のオブジェクトに変更する関数になります。

patchによって第1引数で指定した参照の参照先が変化する様子

patch による参照の変更例2

次は、main は先ほどと同じとして、test_main で下記のように patch の第1引数に 'random.randint' を指定するようにした場合に patch によって参照がどのように変化するのかを確認していきたいと思います。

randintの差し替え(失敗)
from unittest.mock import patch
import main

with patch('random.randint') as randint_mock:
    randint_mock.return_value = 1
    ret = main.roll_dice()

この場合は main から randint を呼び出すと本物の randint が実行されることになります。この理由に関しても、参照の観点から考えると理由がスッキリ理解できると思います。

まず、先ほどと同様に main は randint を参照していることになります。そして、この maintest_main から import されています。

また、patch 実行時には第1引数で指定した「最後の . の前側のオブジェクト(モジュールやクラスなど)」が import されることになりますので、今回の例で言えば、random モジュールが test_main から import されることになります。

さらに、random モジュール内では randint が定義されているため、randomrandint を参照していることになり、この参照は test_main から見れば random.randint となります。つまり、各モジュールの参照関係は下図のようなものになります。

各importによって追加される参照を示す図

そして、test_mainpatch('random.randint') を実行した際には、第1引数で指定した random.randint の身の参照先が patch 内で生成されるモックに変更されることになります。

patchによって第1引数で指定した参照の参照先が変化する様子

ここで注目していただきたいのが、main.randint の参照になります。この参照は main モジュールからの randint への参照であり、この参照の参照先は randint の実体(本物の randint 関数)のままになります。したがって、patch を実行したとしても、main モジュールから randint を呼び出した際には本物の randint が実行されることになります。

逆に、random.randint の参照の参照先はモックに変更されているため、例えば test_main から random.randint を呼び出せばモックが実行されることになります。ですが、今回実現したかったことは main モジュールから呼び出す randint をモックに差し替えることでしたので、これに関しては実現できておらず、この patch の実行は無意味だったことになります。

この例が示すように、特定のオブジェクトへの参照は1つのみとは限りません。実際に、先程の例では randint の実体が main.randintrandom.randint の2つから参照されています。もちろん、モジュールの構成によってはもっと多くの参照から参照されている可能性もあります。

そのため、上手く patch を作用させるためには、patch の第1引数に “モックを利用させたいモジュール” の持つ参照を指定する必要があります。特に単体テスト実施時には、モックはテスト対象モジュールに利用させることになるため、patch の第1引数に “テスト対象モジュール” の持つ参照を指定する必要があります。先程の例で言えば、main モジュールの持つ参照、具体的には main.randint を指定する必要があります。

スポンサーリンク

patch による参照の変更例3

最後の例として、main モジュールが下記の場合の参照の変更例について考えていきたいと思います。今回は main モジュールが randint 関数ではなく random モジュールを import しています。

mainモジュール
import random

def roll_dice():
    ret = random.randint(1, 6)
    return ret

今回の場合、main モジュールが呼び出す randint をモックに差し替えるためには patch の第1引数に 'main.random.randint' or 'random.randint' のどちらかを指定すれば良いです。

この理由も参照の観点で考えると分かりやすいです。

まず、main が実行する patch の第1引数に 'main.random.randint' を指定した場合、各モジュールや関数間の参照関係は下の図のようになります。

main.random.randintをpathの第1引数に指定した場合の参照関係

この場合は分かりやすくて、ここまでの解説を読んでくださった方であれば、patch の第1引数に 'main.random.randint' を指定すれば main が呼び出す randint 関数がモックに差し替えられることがすぐに理解していただけるのではないかと思います。

ただし、patch による参照の変更例1 で示した例で patch の第1引数に指定した文字列('main.randint')と今回指定している文字列が異なる点には注意が必要です。この点からも分かるように、importの仕方によって、patch の第1引数に指定すべき文字列は変更する必要があります。

では、main が実行する patch の第1引数に 'random.randint' を指定した場合の各モジュールや関数間の参照関係はどのようになるでしょうか?この場合は少し複雑で、参照関係は下の図のようになります。

random.randintをpathの第1引数に指定した場合の参照関係

ポイントになるのが、test_main から見ると randint への参照が2パターン存在するという点になります。1つが main を経由したパターンの main.random.randint で、もう1つが random を経由したパターンの random.randint になります。

MEMO

前述の通り、patch を実行すると第1引数で指定した文字列の「最後の . の前側のオブジェクト(モジュールやクラスなど)」が import されることになります

そのため、'random.randint' を指定した場合、test_main から random への参照が追加されることになります

ですが、図を見ていただければ分かる通り、main.random.randintrandom.randint も同じ random モジュールからの randint への参照です。したがって、main.random.randint もしくは random.randint の参照先を変更してやれば、他方側の参照先も変化することになります。

なので、この場合に関しては patch の第1引数に 'main.random.randint' or 'random.randint' のどちらか一方を指定してやれば、main から実行する random.randint をモックに差し替えすることが可能となります。

こんな感じで、オブジェクトのモックへの差し替えは “そのオブジェクトへの参照の参照先をモックに変更すること” であることを理解し、参照関係を考えていけば、patch の第1引数に指定すべき文字列が自然に炙り出すことができると思います。

ただ、先ほどの例のように、同じ差し替えを実現するのに複数パターンの引数の指定の仕方が存在する場合もありますし、参照関係を毎回考えるのは大変です。なので、patch の第1引数への指定の仕方はパターン化してやった方が楽です。

patch の第1引数の指定の仕方(まとめ)

そして、そのパターンが最初に結論として示した下記となります。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

patch を単体テスト時に利用することに限定すれば、大体上記のパターンで引数を指定すれば上手くモックへの差し替えを行うことができると思います。

最初に テスト対象モジュール を指定する

まず、最初に テスト対象モジュール を指定するのは、確実に テスト対象モジュール の持つ参照の参照先を指定するためになります。

この重要性を理解していただくには patch による参照の変更例2 で示した例がわかりやすいと思います。patch による参照の変更例2 で示した例では、下の図のような参照関係となっていました(この例において テスト対象モジュールmain となります)。そして、randint への参照には test_main から見ると2つの経路が存在することが確認できると思います。各importによって追加される参照を示す図

前述の通り、main モジュールの呼び出す randint をモックに差し替えたいのであれば patch の第1引数には 'main.randint' を指定するのが正解となるのですが、ここで言いたいことは、このように複数の経路が存在する場合、経路の選択を間違うと意図した差し替えが実現できない場合があるという点になります。この場合、最初に random を経由する経路を選んでしまうと意図した差し替えが実現できないことになります。

確実に テスト対象モジュール の持つ参照を patch の第1引数に指定するためには、最初に必ず テスト対象モジュール を経由する経路を選択するようにルール決めするのが無難です。そして、そのルールを確実に満たすための指定が、下記における テスト対象モジュール の部分になります。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

要は、patch に指定する文字列の最初の . までの部分は patch を指定するモジュールが最初に選択する経路となります。ここに テスト対象モジュール を指定するようにしておけば、経路としては必ず最初に テスト対象モジュール が選択されるようになります。

patchの第1引数の最初にテスト対象モジュールを指定する意味合いを示す図

最初に必ず テスト対象モジュール が選択されるのであれば、あとは . の後ろ側に テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」 を指定してやれば、差し替えしたいオブジェクトへの参照が複数ある場合でも、確実に テスト対象モジュール の持つ参照を指定することができます。

スポンサーリンク

参照をコードから判断して指定する

また、テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」 に関してはソースコード内のオブジェクトの利用の仕方から特定することができます。

前述の通り、テスト対象モジュールの持つ参照は、テスト対象モジュールでの import の仕方によって異なります。ですが、ソースコード内では、その違いを考慮して import したオブジェクトを利用しているはずです(でなければ、その参照を利用した時点で例外が発生する)。なので、その利用の仕方を見れば、テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照 に何を指定すれば良いかが分かります。

例えば、下記のように randint 関数を利用しているのであれば、このモジュールは random.randint という参照を持っていることが分かりますので、この random.randintテスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照 に指定してやれば良いことになります。

randintへの参照の利用1
ret = random.randint(1, 6)

それに対し、下記のように randint 関数を利用しているのであれば、このモジュールは randint という参照を持っていることが分かりますので、この randintテスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照 に指定してやれば良いことになります。

randintへの参照の利用2
ret = randint(1, 6)

このように、patch の第1引数に 'テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」' を指定するようにしてやれば確実に差し替えたいオブジェクトをモックに差し替えることができますし、patch の第1引数への指定を一意に特定することができます。何より、この考え方はシンプルで分かりやすいと思います。

ということで、以上が patch の第1引数には下記を指定してやれば良いという理由になります。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

特殊なオブジェクトの patch での差し替え

最後に、少し特殊なオブジェクトの patch での差し替えについて解説していきます。

組み込み関数の patch での差し替え

最初に紹介する特殊なオブジェクトは組み込み関数となります。このページで紹介している patch の引数の指定の仕方は、組み込み関数のモックへの差し替えを行う場合にも有効です。組み込み関数は import なしに利用することが可能ですが、このページで紹介している patch の引数の指定の仕方は import の有無に関わらず適用可能です。

例えば、テスト対象モジュールが main で、下記のように main の中から len 関数を利用していたとします。この len 関数は、皆さんもご存知の通り import なしで利用可能です。

lenを利用する例
def count_len(obj):
    ret = len(obj)
    return ret

main は上記のように単に len を呼び出すことで len の実体を実行しているのですから、mainlen という参照を持っており、この参照の参照先は len の実体ということになります。

mainがlenへの参照を持っている様子

で、この len をモックに差し替える場合、この len への参照の参照先をモックに変更すれば良いのですから、テストを実装するモジュールでは下記のように patch の第1引数に 'main.len' を指定すれば良いことになります。

lenを差し替える例
from unittest.mock import patch
import main

with patch('main.len') as len_mock:
    # 略

同様にして、printinput などの組み込み関数もモックへの差し替えを行うことが可能です。

スポンサーリンク

同じモジュールで使用するオブジェクトの差し替え

また、同じモジュール内で使用しているオブジェクトの差し替えを行う場合も、同じ考え方で patch の第1引数を指定すれば良いです。

例えば下記の test_main モジュール(test_main.py)は test_main 内で呼び出している関数 randint をモックに差し替える例となります。

test_main.py
import unittest
from unittest.mock import patch
import random

def RollDice():
    ret = random.randint(1, 6)
    return ret

class TestRollDice(unittest.TestCase):

    def test_dice_1(self):
        '''サイコロの目が1の場合の動作確認'''

        with patch('test_main.random.randint') as randint_mock:
            randint_mock.return_value = 1
            ret = RollDice()

        self.assertEqual(ret, 1)


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

この場合は、テスト対象モジュールと patch を実行するモジュールが共に test_main となり、今まで少し異なるケースとなりますが、patch の第1引数のテスト対象モジュール 部分には素直にテスト対象モジュールとなる test_main を指定してやれば良いです。また、この test_main ではrandintrandom.randint という参照から呼び出していますので、randint をモックに差し替えるのであれば patch の第1引数には 'test_main.random.randint' を指定すれば良いことになります。

このように、組み込み関数の差し替えや同じモジュールで使用しているオブジェクトの差し替えに関しては少しイレギュラーな例にも思えますが、patch の第1引数の指定の仕方の考え方は変わりません。

まとめ

このページでは、Python の patch 関数への第1引数の仕方について解説を行いました!

結論としては、patch 関数への第1引数には下記のような形式の文字列を指定してやれば良いです。

patchへの第1引数への指定
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')

重要なのは、patch 関数は差し替え対象のオブジェクトを他のオブジェクトに上書きすることでオブジェクトの差し替えを実現するのではなく、差し替え対象のオブジェクトへの参照の参照先を他のオブジェクトに変更することでオブジェクトの差し替えを実現しているという点になります。

この点を理解し、各モジュール・クラス・関数間の参照関係を整理していけば、patch 関数の第1引数に指定すべき文字列は自然と炙り出せると思います。

patch 関数を使いこなせるようになればモックを利用した単体テストを効率的に実施することができるようになります。単体テストの導入によりソフトウェアの品質向上や開発効率向上を促進することができますので、是非このページで解説したことは覚えておいてください!

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