スクリプトのお勉強 技術

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

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

執筆者:

関連記事

Python(prophet)で体重予測

最近ダイエットしていて、少しだけ成果が出たので、グラフ表示しようと思ってました。そのついでにこれからの予測もしてみようということで、過去のデータを集め、グラフ表示してみます。 google Colab …

WSL2にOpenCV + Pythonをインストールする

お手軽にやろうと思ってやってみました。作業手順自体を見ると、お手軽ではないかもしれません。 環境 インストールする/した環境は以下の通りです。 WSL2Ubuntu 20.04.2 LTSPython …

コンピュータ系の人のためのCoinhive最高裁の読み方(なぜ無罪だったか)

以下ですべて断定してますが、そもそもからしてすべて想定です。私は法律家ではないので。 前回の記事で、Coinhiveの高裁判決について書きました。 今回は、無罪と決まったCoinhiveの最高裁判決を …

Pipenv vs Poetry

1. はじめに Pythonでお仕事していると、どうしても、環境設定を行う必要があります。 本番環境で動作するように、設定しなければいけないからです。 いろんな状況はあるでしょうが、私がかかわるプロジ …

キーボード買った(Maestro2S 日本語配列)

キーボード購入 キーボードが完全にいかれたので新しいキーボードを買ってきました。 Maestro2S 日本語配列 Maestro2S 日本語配列 です。 画像 実は以前のキーボードとほぼ同じです。違う …

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