2020年9月19日土曜日

pytest でデコレータに対して monkeypatch を適用する方法

概要

Python でデコレータを使う方法を過去に紹介しました
デコレートされている関数を pytest でテストする場合にデコレータは必ず実行されてしまいます
デコレータを monkeypatch したいケースがあったのでコツや回避方法を紹介します

環境

  • macOS 10.15.6
  • Python 3.8.5

テストするコード

以下のようなデコレータを持つコードをテストすることを想定しています

def first_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            message = generate_message(msg)
            return func(*args, **kwargs) + msg
        return wrapper
    return _first_deco

@first_deco(msg="World")
def hello():
    return "Hello"

デコレータ自体のパッチはできないっぽい

いきなり結論なのですが自分が試した感じだと直接デコレータ自体を monkeypatch することはできませんでした
例えば以下のように直接デコレータに対して monkeypatch をしてもパッチした処理はコールされませんでした

import pytest

import deco
from deco import hello

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'first_deco', lambda msg: "Python")
    result = hello()
    assert (result == "HelloPython")

また以下のようにパッチする関数をテスト側で定義してもダメでした
ただ個別で monkeypatch した関数をコールするとちゃんと monkeypatch は当たっている感じでしたが、なぜかデコレートした元の関数をコールするとパッチされた関数は呼ばれません

deco.first_deco(msg="Python")(hello)()

import pytest

import deco
from deco import hello

def dummy_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs) + "Python"
        return wrapper
    return _first_deco

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'first_deco', dummy_deco)
    result = hello()
    assert (result == "HelloPython")

というのを踏まえて対処方法を紹介します

デコレータの内部処理をパッチする

これが一番現実的かなと思います
デコレータの内部で読んでいる処理を外部の関数にしてその関数をパッチする感じです
調べてもこの方法が王道っぽいです

以下では generate_message をデコレータの外に定義してその関数を monkeypatch することで対処しています

def generate_message(msg):
    return msg

def first_deco(msg):
    def _first_deco(func):
        def wrapper(*args, **kwargs):
            message = generate_message(msg)
            return func(*args, **kwargs) + message
        return wrapper
    return _first_deco

@first_deco(msg="World")
def hello():
    return "Hello"

テスト側では generate_message をパッチします

import pytest

from deco import hello, generate_message

def test_first_deco(monkeypatch):
    monkeypatch.setattr(deco, 'generate_message', lambda msg: "Python")
    result = hello()
    assert (result == "HelloPython")

undecorated を使う

そもそもデコレータが邪魔で処理自体が不要という場合は undecorated という便利なライブラリがあるのでこれを使うのも有効です
デコレータの処理は完全にスルーすることになるので注意が必要です

  • pipenv install undecorated
import pytest

from undecorated import undecorated
from deco import hello

def test_first_deco(monkeypatch):
    org_hello = undecorated(hello)
    result = org_hello()
    assert (result == "Hello")

最後に

いろいろ調べましたが直接デコレータを monkeypatch することはできないっぽいです
対処として

  • デコレータの内部処理を外部の関数として定義してそれをパッチする
  • undecorated を使ってデコレータの処理自体をスルーさせる

の 2 パターンかなと思います

0 件のコメント:

コメントを投稿