スクリプトのお勉強 技術

Pythonのmock.patchを使ってみる

投稿日:

単体テストによく使われるMockライブラリ

Pythonで単体テストを行う際、実際のライブラリを使用してしまうと、実際の環境を用意しなければいけません。

例えば、データベースのテストをする際に、データベースの環境を前提とするのではなく、ロジックだけテストしたいことはよくあります。

その際に、データベース等にアクセスするライブラリ側をざっくりと変更してしまう方法がMockによるテスト方法です。

Python3はほぼ標準で用意されている

Python3はmockというライブラリが既に用意されていて、使えるようになっています。

私がやるとできないことが多い。。

でも、私がやることできないことが多いので、試してみます。

テストサンプル1(mailsend)

ディレクトリ構成を以下にします。

Pipfile
Pipfile.lock
lib/
  mail.py
  upper.py
tests/
  test_mail.py
  test_upper.py

以下のような、lib/mail.pyのmailsendクラスをテストするとします。

from smtplib import SMTP


def mailsend(from_addr, to_addr, body):
    msg = {}
    msg['Subject'] = 'test'
    msg['From'] = from_addr
    msg['To'] = to_addr

    # Send the message via our own SMTP server.
    s = SMTP('localhost')
    s.send_message(msg)
    s.quit()

py.test使用によるMockライブラリの使用方法

Mockライブラリを使用したテストコード(tests/test_mail.py)は以下のようになります。以下は、デコレーターとして設定する方法です。

import os
import sys
from mock import patch
from unittest import TestCase

# rootを一つ上のディレクトリに指定
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__name__)), 'lib'))

from mail import mailsend


class TestMail(TestCase):

    @patch('mail.SMTP')
    def test_mail(self, smtp):

        mailsend('from', 'to', 'body')

        for name, args, kwargs in smtp.mock_calls:
            # モック内容の表示
            # print(name, args, kwargs)
            if name == '().send_message':
                calls = args[0]

        self.assertEqual(calls['To'], 'to', 'send_message called')

テストを実行すると以下になります。

$ pipenv run py.test tests/test_mail.py
====================================================================================================== test session starts ======================================================================================================
platform linux -- Python 3.6.8, pytest-5.2.1, py-1.8.0, pluggy-0.13.0
rootdir: /home/tanino/script-plactice/practice_the_script/python/mock
collected 1 item

tests/test_mail.py .

重要なのは@patch('mail.SMTP')です。この意味は、「mailパッケージ内のSMTPモジュールをMockに置き換える」という意味になります。

class TestMail:

    @patch('mail.SMTP')
    def test_mail(self, smtp):

‘mail.SMTP’は、なんとなくですが、私の直感には反する設定です。
何も考えないと、以下のSMTPをMockに変えるのだから

from smtplib import SMTP

smtplib.SMTPだと思うのですが、smtplib.SMTPと設定しても動作しません。
その理由は、以下に書いてあるのですが、、

https://docs.python.org/ja/3/library/unittest.mock.html#where-to-patch

基本的な原則は、オブジェクトが ルックアップ されるところにパッチすることです。

分かるような分からないような説明です。少なくとも私にはわかりませんでした。つまり上記の場合、import「先」にpatchが当たるように、
「パッケージ名.import「先」」
を設定しているというので一応納得ができます。

あとは、Mockを使うときには、以下をよく使います。

for name, args, kwargs in smtp.mock_calls:
    # モック内容の表示
    # print(name, args, kwargs)

mockオブジェクトの中に何が入ってるか分からない場合は、print文で表示してみるのはよくやります。

テストサンプル2

これだけなら比較的簡単なのですが、patchの引数に何を指定すべきかよく分からない場合があります。
それは、間接的にimportしているときです。

例えば、lib/upper.pyという中間ライブラリがあったとして、そのupper.py内部で上記のmailsendが呼び出されているとします。

lib/upper.pyは以下の感じです。

class Upper:

    def middle(self):
        from mail import mailsend

        mailsend('2from', '2to', '2body')

lib/upper.pyをテストするのは、以下のtests/test_upper.pyです。

import os
import sys
from mock import patch
from unittest import TestCase

sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__name__)), 'lib'))

from upper import Upper


class TestMiddle(TestCase):

    @patch('mail.SMTP')
    def test_mail(self, smtp):

        Upper().middle()

        for name, args, kwargs in smtp.mock_calls:
            # モック内容の表示
            # print(name, args, kwargs)
            if name == '().send_message':
                calls = args[0]

        self.assertEqual(calls['To'], '2to', 'send_message called')

以下のように両方通すと通ります。

$ pipenv run py.test tests
====================================================================================================== test session starts ======================================================================================================
platform linux -- Python 3.6.8, pytest-5.2.1, py-1.8.0, pluggy-0.13.0
rootdir: /home/tanino/script-plactice/practice_the_script/python/mock
collected 2 items

tests/test_mail.py .
tests/test_upper.py .

ここでの重要な所は@patch('mail.SMTP')にあります。
mock.patchは、「テストコードから見て」どこのimport「先」をpatchするかが重要で、呼び出す関数がどのようにimportしているかは関係ないのですね。。

ここらへんがいつも混乱して、動作しない原因のような気がします。

おわりに

Mock系は便利ではありますが、使い過ぎると、Mock自体をテストしている感じになってしまうので多用しないようにしています。

これもテストコードでは動作しますが、実際に使うと動かなかったりするんですよね。。そこらへんはしょうがない。。

参考

  • グローバルスコープ自体を変えてしまう方法
    https://atsuoishimoto.hatenablog.com/entry/2013/07/27/000000

-スクリプトのお勉強, 技術

執筆者:

関連記事

Django2.2 でのMySQL5.1対応

「対応」と書きながら、思い切り回避ですが。 マイグレーション時のエラー マイグレーションしたら、以下のエラーになりました。 $ pipenv run python3 manage.py migrate …

SPAMチェック for OCN の開発

前回、Thunderbirdプラグインの概要を書いたので、今回は開発したプラグインについて書きます。 SPAMチェック for OCNとは 以下の機能を持ったThunderbirdのアドオン(プラグイ …

VMware player(workstation) から WSL2に乗り換える

いままで、VMware player(今のVMWare Workstation Player)上で、作って確認していたけど、いちいち起動するのがだるいです。 ついでにやたらとリソースを使用するので、こ …

めっちゃ冷や汗。。(^^;;)

めっちゃ冷や汗が出たので、書いておこう。。 $ ls [‘ [‘という名のファイルを無意識に作ったらしい。もちろん意図的ではない。 なんとなく、消そうとして、、、 $ rm -rf “[\ …

Mojolicious XML-RPC Pluginの開発(2)

はじめに 「Mojolicious XML-RPC Pluginの開発」のその2です。 本内容では、前回の内容を踏まえ、XML-RPC実装を、Mojoliciousのプラグイン機能を使用して実装しよう …