このページでは、Python の patch
への第1引数の仕方についての解説を行います。
patch
は unittest.mock
モジュールから提供される関数の1つで、主にモックと他のオブジェクトとの差し替えを行う目的で利用する関数になります。mock
や patch
に関しては下記ページで紹介していますので、まだご存知ない方は事前に下記ページを読んでみていただければと思います。
モックを利用することで Python での単体テストを効率的に実施することが可能となります。ただし、このモックを上手く利用するためには patch
を使いこなす必要があります。ですが、この patch
の第1引数の指定の仕方の考え方が少し難しいため、それに伴い patch
を使いこなす難易度も高くなっています。
このページでは、誰でも patch
やモックを使いこなせるようになるように、patch
の第1引数の指定の仕方について解説していきたいと思います。
Contents
patch
関数への第1引数の仕方
最初に結論を述べておくと、patch
への第1引数は下記のような形式で指定を行えば良いです。他の形式で指定しても意図した通りに patch
関数を動作させることができる場合もありますが、下記の形式で指定するのが一番シンプルで分かりやすいと思います。
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')
特に テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」
の部分の意味が分からないと言う方もおられるかもしれませんが、ここからの解説も読み進めていただければ理解できると思います。
具体例を示しておくと、単体テストでのテスト対象モジュールが下記のような main
であり、この main
から呼び出している randint
をモックに差し替えたい場合、
import random
def roll_dice():
ret = random.randint(1, 6)
return ret
テスト対象モジュール
は main
となり、さらに テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」
は random.randint
となります。したがって、patch
関数によって main
の呼び出す randint
をモックに差し替えたい場合は patch
関数の第1引数に 'main.random.randint'
を指定すれば良いことになります。
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引数の指定時の注意点
これは個人的な意見ですが、mock
や patch
を利用する上で一番難しい点は、このページの本題としている patch
への第1引数の指定の仕方だと思っています。公式の mock
の説明ページを見ても、言いたいことはなんとなく分かるけど結局どう指定すれば良いのかが上手く飲み込めませんでした。この難しさを感じたことが、この記事を作成したキッカケとなります。
具体的に難しいと感じたのは「モジュールの import
の指定の仕方によって patch
への第1引数の指定の仕方も変える必要がある」という点になります。ということで、patch
を利用する際には、この注意点に気をつけながら第1引数を指定する必要があります。ただ、最初に紹介した形式であれば、自然とこの注意点を考慮した上で 第1引数の指定が行われることになります。
ここからは、まずは具体例で「モジュールの import
の指定の仕方によって patch
への第1引数の指定の仕方も変える必要がある」という点について確認していきたいと思います。
スポンサーリンク
patch
が上手く作用する例
ここでは、main
モジュールを (main.py
) をテスト対象モジュールとし、さらに test_main
モジュール(test_main.py
)から main
を import
して main
で定義される関数のテストを行う構成を前提に説明をしていきたいと思います。また、このページの主題となる patch
は test_main
から実行するものとして解説していきます。
まずは、main
のコードが下記である場合に、この main
で利用されている randint
を test_main.py
からモックで差し替えてテストを行うことを考えていきます。
import random
def roll_dice():
ret = random.randint(1, 6)
return ret
この場合、test_main
で main.roll_dice
を実行する前に、下記のように patch
の第1引数に 'random.randint'
を指定して patch
を実行すれば、意図通り main
から呼び出しされる 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
等の特別な引数を指定しないからになります。この辺りは下記ページで解説していますので、詳しくは下記ページをご参照ください。
patch
が上手く作用しない例
ですが、下記のように main
での import
の仕方を変更した場合、先ほどと同様に test_main
で patch
の第1引数に 'random.randint'
を指定すると、main
から呼び出しされる randint
のモックへの差し替えが行われません。つまり、main
で randint
を呼び出すと、モックではなく本物の randint
が実行されることになります。
from random import randint
def roll_dice():
ret = randint(1, 6)
return ret
このように、テスト対象のモジュールで他のモジュールを import
する際の import
の仕方を変更すると、今まで上手く patch
が作用していたものが上手く作用しなくなります。これを解決するためには、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_main
が main
を import
し、test_main
から patch
を実行した上で、main
で定義される関数をテストしようとしているものとして解説していきます。
patch
による参照の変更例1
まずは、下記のような main
モジュールについて考えてみましょう!
前述の通り、この main
は test_main
から import
されるものであるとします。そして、この main
が呼び出す randint
をモックに差し替えることを考えていきます。
from random import randint
def roll_dice():
ret = randint(1, 6)
return ret
import
とは、import
先のモジュールやクラス・関数などが利用できるよう、それらの参照を import
元のモジュールに追加することであると考えられます。
上記の場合であれば、まず test_main
は main
を import
することで、main
モジュールを main
という名前の「参照」で参照することになります。さらに、main
は from import
によって random
モジュールから randint
関数を import
することになり、この import
によって main
から randint
関数が参照されることになります。そして、その参照は test_main
から見ると main.randint
となります。
このような参照関係にあるため、test_main
は下記を実行することで randint
を呼び出すことができます。
import main
print(main.randint(1, 6))
前述の通り、オブジェクトのモックへの差し替えとは「そのオブジェクトへの参照の参照先をモックに変更すること」となります。そして、randint
は main
から参照されており、test_main
から見れば、その参照は main.randint
となります。
さらに、これも前述の通り、patch
の第1引数には “差し替えたいオブジェクトへの参照” を指定します。したがって、main
から呼び出しされる randint
をモックに差し替えるためには、test_main
から実行する patch
関数の第1引数に 'main.randint'
を指定してやれば良いことになります。
例えば、下記のような test_main
で patch
を実行するようにすれば、
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
による参照の変更例2
次は、main
は先ほどと同じとして、test_main
で下記のように patch
の第1引数に 'random.randint'
を指定するようにした場合に patch
によって参照がどのように変化するのかを確認していきたいと思います。
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
を参照していることになります。そして、この main
は test_main
から import
されています。
また、patch
実行時には第1引数で指定した「最後の .
の前側のオブジェクト(モジュールやクラスなど)」が import
されることになりますので、今回の例で言えば、random
モジュールが test_main
から import
されることになります。
さらに、random
モジュール内では randint
が定義されているため、random
も randint
を参照していることになり、この参照は test_main
から見れば random.randint
となります。つまり、各モジュールの参照関係は下図のようなものになります。
そして、test_main
で patch('random.randint')
を実行した際には、第1引数で指定した random.randint
の身の参照先が patch
内で生成されるモックに変更されることになります。
ここで注目していただきたいのが、main.randint
の参照になります。この参照は main
モジュールからの randint
への参照であり、この参照の参照先は randint
の実体(本物の randint
関数)のままになります。したがって、patch
を実行したとしても、main
モジュールから randint
を呼び出した際には本物の randint
が実行されることになります。
逆に、random.randint
の参照の参照先はモックに変更されているため、例えば test_main
から random.randint
を呼び出せばモックが実行されることになります。ですが、今回実現したかったことは main
モジュールから呼び出す randint
をモックに差し替えることでしたので、これに関しては実現できておらず、この patch
の実行は無意味だったことになります。
この例が示すように、特定のオブジェクトへの参照は1つのみとは限りません。実際に、先程の例では randint
の実体が main.randint
と random.randint
の2つから参照されています。もちろん、モジュールの構成によってはもっと多くの参照から参照されている可能性もあります。
そのため、上手く patch
を作用させるためには、patch
の第1引数に “モックを利用させたいモジュール” の持つ参照を指定する必要があります。特に単体テスト実施時には、モックはテスト対象モジュールに利用させることになるため、patch
の第1引数に “テスト対象モジュール” の持つ参照を指定する必要があります。先程の例で言えば、main
モジュールの持つ参照、具体的には main.randint
を指定する必要があります。
スポンサーリンク
patch
による参照の変更例3
最後の例として、main
モジュールが下記の場合の参照の変更例について考えていきたいと思います。今回は main
モジュールが randint
関数ではなく random
モジュールを import
しています。
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'
を指定した場合、各モジュールや関数間の参照関係は下の図のようになります。
この場合は分かりやすくて、ここまでの解説を読んでくださった方であれば、patch
の第1引数に 'main.random.randint'
を指定すれば main
が呼び出す randint
関数がモックに差し替えられることがすぐに理解していただけるのではないかと思います。
ただし、patch による参照の変更例1 で示した例で patch
の第1引数に指定した文字列('main.randint'
)と今回指定している文字列が異なる点には注意が必要です。この点からも分かるように、import
の仕方によって、patch
の第1引数に指定すべき文字列は変更する必要があります。
では、main
が実行する patch
の第1引数に 'random.randint'
を指定した場合の各モジュールや関数間の参照関係はどのようになるでしょうか?この場合は少し複雑で、参照関係は下の図のようになります。
ポイントになるのが、test_main
から見ると randint
への参照が2パターン存在するという点になります。1つが main
を経由したパターンの main.random.randint
で、もう1つが random
を経由したパターンの random.randint
になります。
前述の通り、patch
を実行すると第1引数で指定した文字列の「最後の .
の前側のオブジェクト(モジュールやクラスなど)」が import
されることになります
そのため、'random.randint'
を指定した場合、test_main
から random
への参照が追加されることになります
ですが、図を見ていただければ分かる通り、main.random.randint
も random.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('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')
patch
を単体テスト時に利用することに限定すれば、大体上記のパターンで引数を指定すれば上手くモックへの差し替えを行うことができると思います。
最初に テスト対象モジュール
を指定する
まず、最初に テスト対象モジュール
を指定するのは、確実に テスト対象モジュール
の持つ参照の参照先を指定するためになります。
この重要性を理解していただくには patch による参照の変更例2 で示した例がわかりやすいと思います。patch による参照の変更例2 で示した例では、下の図のような参照関係となっていました(この例において テスト対象モジュール
は main
となります)。そして、randint
への参照には test_main
から見ると2つの経路が存在することが確認できると思います。
前述の通り、main
モジュールの呼び出す randint
をモックに差し替えたいのであれば patch
の第1引数には 'main.randint'
を指定するのが正解となるのですが、ここで言いたいことは、このように複数の経路が存在する場合、経路の選択を間違うと意図した差し替えが実現できない場合があるという点になります。この場合、最初に random
を経由する経路を選んでしまうと意図した差し替えが実現できないことになります。
確実に テスト対象モジュール
の持つ参照を patch
の第1引数に指定するためには、最初に必ず テスト対象モジュール
を経由する経路を選択するようにルール決めするのが無難です。そして、そのルールを確実に満たすための指定が、下記における テスト対象モジュール
の部分になります。
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')
要は、patch
に指定する文字列の最初の .
までの部分は patch
を指定するモジュールが最初に選択する経路となります。ここに テスト対象モジュール
を指定するようにしておけば、経路としては必ず最初に テスト対象モジュール
が選択されるようになります。
最初に必ず テスト対象モジュール
が選択されるのであれば、あとは .
の後ろ側に テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」
を指定してやれば、差し替えしたいオブジェクトへの参照が複数ある場合でも、確実に テスト対象モジュール
の持つ参照を指定することができます。
スポンサーリンク
参照をコードから判断して指定する
また、テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」
に関してはソースコード内のオブジェクトの利用の仕方から特定することができます。
前述の通り、テスト対象モジュールの持つ参照は、テスト対象モジュールでの import
の仕方によって異なります。ですが、ソースコード内では、その違いを考慮して import
したオブジェクトを利用しているはずです(でなければ、その参照を利用した時点で例外が発生する)。なので、その利用の仕方を見れば、テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照
に何を指定すれば良いかが分かります。
例えば、下記のように randint
関数を利用しているのであれば、このモジュールは random.randint
という参照を持っていることが分かりますので、この random.randint
を テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照
に指定してやれば良いことになります。
ret = random.randint(1, 6)
それに対し、下記のように randint
関数を利用しているのであれば、このモジュールは randint
という参照を持っていることが分かりますので、この randint
を テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照
に指定してやれば良いことになります。
ret = randint(1, 6)
このように、patch
の第1引数に 'テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」'
を指定するようにしてやれば確実に差し替えたいオブジェクトをモックに差し替えることができますし、patch
の第1引数への指定を一意に特定することができます。何より、この考え方はシンプルで分かりやすいと思います。
ということで、以上が patch
の第1引数には下記を指定してやれば良いという理由になります。
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')
特殊なオブジェクトの patch
での差し替え
最後に、少し特殊なオブジェクトの patch
での差し替えについて解説していきます。
組み込み関数の patch
での差し替え
最初に紹介する特殊なオブジェクトは組み込み関数となります。このページで紹介している patch
の引数の指定の仕方は、組み込み関数のモックへの差し替えを行う場合にも有効です。組み込み関数は import
なしに利用することが可能ですが、このページで紹介している patch
の引数の指定の仕方は import
の有無に関わらず適用可能です。
例えば、テスト対象モジュールが main
で、下記のように main
の中から len
関数を利用していたとします。この len
関数は、皆さんもご存知の通り import
なしで利用可能です。
def count_len(obj):
ret = len(obj)
return ret
main
は上記のように単に len
を呼び出すことで len
の実体を実行しているのですから、main
は len
という参照を持っており、この参照の参照先は len
の実体ということになります。
で、この len
をモックに差し替える場合、この len
への参照の参照先をモックに変更すれば良いのですから、テストを実装するモジュールでは下記のように patch
の第1引数に 'main.len'
を指定すれば良いことになります。
from unittest.mock import patch
import main
with patch('main.len') as len_mock:
# 略
同様にして、print
や input
などの組み込み関数もモックへの差し替えを行うことが可能です。
スポンサーリンク
同じモジュールで使用するオブジェクトの差し替え
また、同じモジュール内で使用しているオブジェクトの差し替えを行う場合も、同じ考え方で patch
の第1引数を指定すれば良いです。
例えば下記の test_main
モジュール(test_main.py
)は test_main
内で呼び出している関数 randint
をモックに差し替える例となります。
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
ではrandint
を random.randint
という参照から呼び出していますので、randint
をモックに差し替えるのであれば patch
の第1引数には 'test_main.random.randint'
を指定すれば良いことになります。
このように、組み込み関数の差し替えや同じモジュールで使用しているオブジェクトの差し替えに関しては少しイレギュラーな例にも思えますが、patch
の第1引数の指定の仕方の考え方は変わりません。
まとめ
このページでは、Python の patch
関数への第1引数の仕方について解説を行いました!
結論としては、patch
関数への第1引数には下記のような形式の文字列を指定してやれば良いです。
patch('テスト対象モジュール.テスト対象モジュールの持つ「差し替え対象のオブジェクトへの参照」')
重要なのは、patch
関数は差し替え対象のオブジェクトを他のオブジェクトに上書きすることでオブジェクトの差し替えを実現するのではなく、差し替え対象のオブジェクトへの参照の参照先を他のオブジェクトに変更することでオブジェクトの差し替えを実現しているという点になります。
この点を理解し、各モジュール・クラス・関数間の参照関係を整理していけば、patch
関数の第1引数に指定すべき文字列は自然と炙り出せると思います。
patch
関数を使いこなせるようになればモックを利用した単体テストを効率的に実施することができるようになります。単体テストの導入によりソフトウェアの品質向上や開発効率向上を促進することができますので、是非このページで解説したことは覚えておいてください!