5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Intimate MergerAdvent Calendar 2024

Day 5

【Python】Patchを使ったMockのハマりポイント

Last updated at Posted at 2024-12-04

こんにちは。

本記事は株式会社インティメート・マージャーのAdvent Calendar 2024 5日目の記事になります。

突然ですが皆さん、mockしてますか?
mockしたいけどやり方がわからない。mockしようとしたけどうまくいかない、などあると思います。

そこで今回はpatchを使ったmockの間違えやすいポイントの紹介をします。

TL;DR

patchを使うときはimportfrom importの違いを注意深く見るのが大事。

そもそもmockとは

mockとは、クラスやクラスメソッド、オブジェクトなどプログラムの一部を置き換えるものです。
置き換えた箇所がどのように使用されているかを調べたり、テストでは動いてほしくない箇所をmockで置き換えてあげることで対象の箇所を動かさないままテストを行うことができるなど大変便利な存在です。

Pythonのunittest.mockのドキュメント

関数の定義されたファイルを直接mockする例

以下のファイル構成で、簡単なmockの例を挙げます。
単純なクラスであれば、この例に従うことでmockができるかと思います。

ファイル構成
.
├─ main_a.py
└─ test.py

まず、main_aをテストする例を挙げます。

main_a.py
def print_example():
    print("example")

if __name__ == "__main__":
    print_example()
test.py
import unittest
from unittest.mock import patch

import main_a


def example_mock():
    print("mock")

class Tester(unittest.TestCase):
    def test_main_a(self):
        main_a.print_example()

        with patch("main_a.print_example",side_effect=example_mock):
            main_a.print_example()


unittest.main()

testを実行すると...

test.pyの実行結果
example
mock
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

test.pyでは、main_a.print_example()を2回呼び出しています。
ここで、一度目の呼び出しと二度目の呼び出しで出力が異なっていることが分かると思います。
正しくmockを行うことができれば、このように特定の部分を置き換えてテストを実行することができます。
とても簡単にmockをすることができました。

さて、それではどのようなところに落とし穴があるというのでしょうか?
次の例をご覧ください。

関数をimportしたファイルをmockする例

ファイル構成
.
├─ main_a.py
├─ main_b.py
├─ main_c.py
└─ test.py

次は、先ほどのmain_aをimportして使用しているファイル、main_bとmain_cをテストする例を挙げます。

main_b
import main_a


def main():
    # ここはMockしたくない
    print("main_b is working!")
    # ここだけMockしたい
    main_a.print_example()


if __name__ == "__main__":
    main()
main_c
from main_a import print_example


def main():
    # ここはMockしたくない
    print("main_c is working!")
    # ここだけMockしたい
    print_example()


if __name__ == "__main__":
    main()

main_bとmain_cの間に違いはほとんどありません。main_aのimportの仕方が異なるだけです。

test
import unittest
from unittest.mock import patch

import main_b as main_b
import main_c as main_c


def example_mock():
    print("mock")

class Tester(unittest.TestCase):
    def test_main_b(self):
        with patch("main_a.print_example",side_effect=example_mock):
            main_b.main()

    def test_main_c(self):
        with patch("main_a.print_example",side_effect=example_mock):
            main_c.main()


unittest.main()

testを実行すると...

test.pyの実行結果
func_b is working!
mock
.func_c is working!
example
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

main_bの方はmockができていますが、main_cの方はmockができておらず、そのままprint_exampleが実行されてしまっています。
エラーにもならないため、複雑なテストを書いている際にこの現象を踏んでしまうと気づき辛く大変厄介です。

どうしてこうなってしまったのか

mockのパッチ先についてのドキュメント

重要なのは、 SomeClass が使われている (もしくはルックアップされている) 場所にパッチすることです。

引用先の例はクラスですが、関数である場合も同様で、import main_afrom main_a import print_exampleの間での、それぞれのオブジェクトを利用する時に見ている先の違いが原因でした。

def test_main_c(self):
-    with patch("main_a.print_example",side_effect=example_mock):
+    with patch("main_c.print_example",side_effect=example_mock):
main_c.main()

このようにpatchを当てる先を変えることで、main_cに対してもmockをすることができます。

まとめ

本記事では、patchによるmockを行う際に間違えやすいポイントを紹介しました。
mockを行う先のファイルが、importを使っているのかfrom importを使っているのかを注意深く見る必要があることを理解できたかと思います。

あとがき

複雑なコードのテストを書いている時は意外とこの原因に気が付かないこともあるかと思います。そういった時にこの記事を思い出していただけると幸いです。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?