デコレータを使った関数の置き換え(dynamic_scope的な)

djangoで開発をしていて、実機でのテストがめんどい部分があった。
views関数の中で、想定した状態の時に、意図した通りの分岐が行われるかテストしたい。

そのためには、関数を置き換えられると良さそう。そんなわけで、`with_dummy`というデコレータを作成した。

対象

  • テストを書こうとしている人
  • テストで、実行時の分岐のルートを確認したい人
  • デコレータ、クロージャを理解している人

code

コードはこんな感じ。

import sys

def with_dummy(target_fn, dummy_fn, module=None):
    if module is None:
        module_dict = globals()
    else:
        module_dict = module.__dict__

    target_fn_name = target_fn.__name__
    original =  target_fn

    def _with_dummy(function):
        if not target_fn_name in module_dict:
            message = "`%s` is not found in %s\n" % (target_fn_name, module)
            sys.stderr.write(message)

        def __with_dummy(*args, **kw):
            module_dict[target_fn.__name__] =  dummy_fn
            result =  function(*args, **kw)
            module_dict[target_fn.__name__] =  original
            return result
        return __with_dummy
    return _with_dummy

引数としては、target_fnに置き換えたい関数、dummy_fnに置き換え後の処理をする関数を渡す。
最後のmoduleは関数が定義されているモジュールを渡すようにする。

使い方

ライブラリからインポートした関数と同じファイルで定義した関数とで微妙に使い方が異なる。

同じファイル(モジュール)で定義した関数の置き換え

同じモジュールで定義した関数の場合の使い方は以下。

def f(n):
    """渡された引数が偶数の時gを呼び出し、奇数の時hを呼び出す。"""
    return g(n) if n % 2 == 0 else h(n)

def g(n):
    print "g", n

def h(n):
    print "h", n

@with_dummy(g, lambda x : sys.stderr.write("ggggg %s\n" % x))
def f2(n):
   """ fを実行する。しかし、内部で実行されるgはwith_dummyで置き換えられたもの"""
    return f(n)

f(4)
f2(6)  # ここでだけgが置き換わる。
f(10)

## result
# g 4
# ggggg 6
# g 10

fは偶数か奇数かで分岐する関数。f2はfの実行をgを違う処理に置き換えて実行する関数。
f2からの実行では、gは"ggggg n"を出力する関数に変わっている。

ライブラリからインポートした関数の置き換え

上で定義した関数を今度はfgh.pyで定義して、これをインポートして使ってみる。

# fgh.py

def f(n):
    return g(n) if n % 2 == 0 else h(n)

def g(n):
    print "g", n

def h(n):
    print "h", n

fghをインポートして使う。同様にgを置き換えてみる。

import fgh

@with_dummy(fgh.g, lambda x : sys.stderr.write("ggggg %s\n" % x), module=fgh)
def f2(n):
    return fgh.f(n)


fgh.f(4)
f2(6)
fgh.f(10)

## result
# g 4
# ggggg 6
# g 10

viewsで定義された関数のルート確認test

ディレクトリ構成

myqpp
|___ views.py
|___ tests.py

views.pyに定義した関数のテスト。

views.py

def index(request):
    print "hey, hey index is called"
    return HttpResponse("index")

このindex適切にHttpResponseを呼んでいるかテスト。以下のようなテストを書けば良い。

tests.py

# slackoff
import myapp.views as v
import django.http as dh

class ViewTestCase(TestCase):
    def test_index(self):
        @with_dummy(dh.HttpResponse, lambda x : self.assertEqual(x, "index"), dh)
        def use_dummy_httpResponse():
            return v.index(None)
        return use_dummy_httpResponse()
result
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Creating test database 'default'...
Creating table auth_permission
....
Installing index for auth.Permission model
....
No fixtures found.
hey, hey index is called
Destroying test database 'default'...

上手くいっている。
関数の中でさらに関数が読み込まれた場合については、考慮していないけれど。