スクリプトのお勉強 技術

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

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

執筆者:

関連記事

リモート実家帰りしてみる

このご時世、実家には直接帰れないけど、1月には一応実家帰り的な感じでリモート実家帰りをしようかと思いました。 リモートは大変。。 一応実家には自分で設置したインターネットや無線LANがあるので、Zoo …

Githubからお告げ

Githubからお告げが来た お告げってのは大げさですが。 Github にpackage-lock.jsonを置いておくと、Githubが脆弱性検査をして、メールを知らせてくれます。 js-yaml …

React.js の Ant Design使ってみる(DateTimePicker編)

DateTimePickerサンプル DateTimePickerを使うサンプルで、いいのがなかなかないです。Dateだけとか、Timeだけってのはあるのですが。 ということで作ってみようと思いました …

go 1.16 でviperを使って設定ファイルを読みだすプログラムを作ってみた

他にたくさんあるけど こんな感じの内容はたくさんあると思いますが、、goの初心者がなんとなくgoの手習いとして、まずは設定ファイルを読みだすことをしてみようと思いました。 go動作環境 windows …

PythonでPKCS#12を使用して暗号/復号する

1. はじめに 仕事でVPN関係のシステム開発をすることになりました。まずは暗号機能の基本を思い出すため、Pythonで、PKCS#12の公開鍵で暗号、秘密鍵で復号するプログラムを作ってみようと思いま …

google オプトアウト Click here to opt-out.