2020年7月2日木曜日

pytest の monkeypatch を使って簡単モック作成

概要

pytest の monkeypatch という機能を使うとインスタンスメソッドや変数を簡単にモックすることができます
今回はいくつかある monkeypatch を使ったモックの作成方法を紹介します

環境

  • macOS 10.15.5
  • Python 3.8.3

インストール

  • pipenv install pytest

サンプルコード

テスト対象のコードを用意します
今回はわかりやすいように外部にアクセスするメソッドを作っています

  • vim user.py
from http.client import HTTPSConnection


class User():
    FAVORITE_FRAMEWORKS = {
        "ruby": "sinatra",
        "swift": "spritekit",
        "python": "flask"
    }

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def hello(self):
        return("%s,%i" % (self.name, self.age))

    def access(self):
        con = HTTPSConnection("kaka-request-dumper.herokuapp.com")
        con.request('GET', '/')
        res = con.getresponse()
        return res.read().decode()

テストコード

hello と access のテストをするコードを作成します
以下ではわざとエラーになるようにしてます

  • vim test_user.py
from user import User

def test_hello(monkeypatch):
    u = User("hawksnowlog", 10)
    assert(u.hello() == "hawksnowlog,5")

def test_access(monkeypatch):
    u = User("hawksnowlog", 10)
    assert(u.access() == '{"key":"value"}')

とりあえず実行

  • pipenv run pytest

どちらのテストもエラーになることが確認できると思います

メソッドをモックしてエラーにならないようにしてみる

今回はお試しなのでモックを使うことでテストがエラーにならないようにしてみます
monkeypatch.setattr を使うことで User クラス内にある helloaccess メソッドをモックし期待するレスポンスが返ってくるように修正します

  • vim test_user.py
from user import User

def test_hello(monkeypatch):
    # hello メソッドにパッチを当ててレスポンスを操作する
    # return は callable でなければならない
    monkeypatch.setattr(User, "hello", lambda self: "hawksnowlog,5")
    u = User("hawksnowlog", 10)
    assert(u.hello() == "hawksnowlog,5")

def test_access(monkeypatch):
    # モック用の内部メソッドを作成してそれを登録することもできる
    def mock_return(self):
        return '{"key":"value"}'

    monkeypatch.setattr(User, "access", mock_return)
    u = User("hawksnowlog", 10)
    assert(u.access() == '{"key":"value"}')

setattr は 3 つの引数を指定します
それぞれ「クラス」「メソッド名」「モック」になります
モックに関しては callable でなければなりません
モック用のメソッドを用意してそれを指定しても良いですし lambda 式が使える Python なのであれば lambda を指定しても OK です

モックするメソッドが引数を持つ場合は引数が同じになるようにしましょう

基本的にはこの setattr が使えれば問題ないですが他にも monkeypatch することができます

辞書の値を変更する

例えば定数として持っている辞書の値を変更することができます
setitem というパッチを当てます

def test_langs(monkeypatch):
    monkeypatch.setitem(User.FAVORITE_FRAMEWORKS, "ruby", "rails")
    assert(User.FAVORITE_FRAMEWORKS["ruby"] == "rails")

ちなみに辞書から要素を削除する delitem というメソッドもあります

他には

  • delattr で特定の関数や属性を削除することができます
  • setenv は環境変数をテスト時に上書きすることができます
  • delenv は環境変数をテスト時に削除することができます
  • chdir はテスト時の実行ディレクトリを変更することができます
  • syspath_prependsys.path で参照できる配列の先頭に指定の仮想パスを追加することができます
def test_syspath(monkeypatch):
    monkeypatch.syspath_prepend("hoge")
    assert(sys.path[0] == "hoge")

最後に

pytest の monkeypatch を試してみました
setattr が基本なのでこれが使えれば大体はカバーできると思います
かなり強力なのでこれが使いたいためにテストフレームワークを pytest にする人もいるんじゃないかなと思います

参考サイト

0 件のコメント:

コメントを投稿