6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

pytest-mock についてざっくり紹介

Last updated at Posted at 2020-12-28

ここでは簡単に pytest-mock について紹介します。

モックの意義

モジュールや flask アプリをテストしたい場合は、pytest でテストケースを書くとは多々あるかと思います。
しかし、テスト対象に HTTP と外部とコミュニケーションする処理が含まれている場合、どうしますか。
テスト時にエンドポイントを作成したり、あるいはテスト時に if 分岐を作成するという方法があります。しかし、エンドポイント作成するとなるとバックエンド変更しないといけなかったり、if だと実装が煩雑になるという問題が生じます。
そこで登場するのが pytest-mock です。

pytest-mockとは

unittest.mock の薄いラッパーです。既存ライブラリや関数等をモック(既存品の模型)するプラグインライブラリで、pytest 時にモックされたオブジェクトの実挙動を回避することができます。例えば、テスト対象で HTTP の POST をするライブラリを使用していた場合、モックで置き換えることにより実際の POST 操作を回避することができます。もっと噛み砕くと「あたかも POST する、けど 実は POST していない」です。

pytest-mock のインストール方法

pip install pytest-mock

あとは pytest で mocker と言う引数を呼ぶだけで mocker が使えるようになります。

patch

まず pytest-mock がないテストを紹介します。

mock_project
├── __init__.py
├── some_file.py
└── tests
    ├── __init__.py
    └── test_some_file.py
some_file.py
from random import random
def generate_random():
    return random()
tests/test_some_file.py
import pytest
from mock_project import some_file
class TestA:
    def test_01(self):
        assert some_file.generate_random() == 1

これは確実に落ちます。実際にランダムで数字を生成したとして、1 になる確率は極わずかです。
ではどうするか?

ここで、モックを使います。

モックする際には import 先のオブジェクトを指定する必要があります。

以下 some_file にある randomfake_random でモックします。

tests/test_some_file.py
import pytest
import some_file

def fake_random():
    return 1

class TestA:
    def test_01(self, mocker):
        mocker.patch.object(some_file,"random",fake_random)
        assert some_file.generate_random() == 1

実際に動作してみると 以下の通りです。(加工しました)

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.79s ===========

もう一つの patch

mocker.patch.object 意外にも mocker.patch を使ってモックする方法があります。
mocker.patch.object は 第1引数に モジュールを入力しますが、mocker.patch は string を入力します。
以下のコードをご覧ください。

tests/test_some_file.py
import pytest
from mock_project import some_file 
import random

# SAMPLE ONE
def fake_random():
    return 1

class TestA:
    def test_01(self, mocker):
        mocker.patch.object(some_file,"random",fake_random)
        assert some_file.generate_random() == 1

    def test_02(self, mocker):
        mocker.patch("mock_project.some_file.random",fake_random)
        assert some_file.generate_random() == 1

実行すると以下の通りです。

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py .. [100%]

=========== 1 passed in 0.79s ===========

期待通りですね。
両方ともできることは同じなのでどっち使うかは状況によって変わったり、好みの問題だったりしますが、もし私が選ぶとしたらできる限り mocker.patch.object を選びます。

関数モック

引数のある関数をモックすることも可能です。

some_file.py
def amplify_10(x):
    return 10 * x

def process_10(x):
    return amplify_10(x)
tests/test_some_file.py
import some_file
import random

def fake_amplify_10(x):
    return  int(x/10)

class TestB:

    def test_01(self, mocker):
        mocker.patch.object(some_file,"amplify_10",fake_amplify_10)
        assert some_file.process_10(10) == 1
        print("amplify test")

実行すると以下の通りです。

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.60s ===========

クラスやライブラリのモック

以下のように他ファイルにおけるクラスやモジュールをモックすることも可能です。

some_file.py
import MeCab
def parse_sent(x):
    mecab = MeCab.Tagger("-d /tmp/xxxxx")
    return mecab.parse(x).split()
tests/test_some_file.py
class TestC:

    def test_01(self, mocker):
        mock_MeCab = mocker.Mock()
        mock_MeCab_Tagger = mocker.Mock()

        def fake_parse(x):
            return "a b c"
        mock_MeCab_Tagger.parse = fake_parse
        
        mock_MeCab.Tagger = mocker.Mock(return_value=mock_MeCab_Tagger)

        mocker.patch.object(some_file,"MeCab",mock_MeCab)
        
        res = some_file.parse_sent("ハロー、今日わ")

        assert res == ["a","b","c"]

これも実行すると以下のようになります。

>> pytest test_mock.py
=========== test session starts ===========
platform darwin -- Python 3.7.3                                                                                    

tests/test_some_file.py . [100%]

=========== 1 passed in 0.58s ===========

こちらについて少し解説すると、ここで出てくる mocker.Mockreturn_value は call 時に何を返すかを意味します。

x=mocker.Mock(return_value="hi")
print(x("zzzzzzz")) # hi

つまり上記のクラスを一個一個見ていくと以下のようになります。

some_file.py
# MeCab -> mock_MeCab
mecab = mock_MeCab.Tagger("-d /tmp/xxxxx")
# mock_MeCab.Tagger -> mocker.Mock(return_value=mock_MeCab_Tagger) -> mock_instance
mecab = mocker.Mock(return_value=mock_MeCab_Tagger)("-d /tmp/xxxxx") # 2つ目の引数はcall
# mock_instance("xxx") -> mock_MeCab_Tagger
mecab.parse = mock_MeCab_Tagger.parse
mecab.parse = fake_parse

従って some_file.py は以下を実行します。

some_file.py
def parse_sent(x):
    return fake_parse(x).split()

まとめ

上記のように pytest-mock について紹介しました。今回は関数の上書きとモジュールの上書きについて紹介しましたが、何回 call されたか、どの様な引数でcall されたかも記録できます。もし使う機会があれば、ぜひぜひ使ってみてください。

6
4
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
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?