2011-11-13

python の mock ライブラリを使ってみる


最近 mock ライブラリを使うようになりました。

能書き

(ここは単体テストとモックの意義が分かってる人には、価値ゼロです。)

単体テストというのは、本来、あるコンポーネントの依存先に影響されないように、対象をテストします。が、これまでは比較的てきとーで、依存先のテストが通っていれば、依存先を完全に分離せずにやってきました。

これには2つ問題があって、(1) そもそもそれは単体テストではない、(2) 依存先が外部だったらどうするんだ、と。例えば、Twitter からタイムラインをとってくる、とかですね。

想定するテスト対象

def get_user_timeline(user)
    """タイムラインをとってきて、辞書で返す"""
    twitter = Twitter()
    response = twitter.get_timeline(user.id, user.access_token)
    timeline = [{'text': tweet.text} for tweet in response['tweets']]
    return timeline

辞書にレンダリングを分離すべきとかありますが、いまは Twitter から取ってくる箇所だけ考えます。

テストの度に本当に Twitter にアクセスしていては単体テストになりません。ってなわけで、 mock ライブラリを使います。

インストール

easy_install mock 

で、おk。

Mock クラス

Mock のインスタンスのプロパティは適当にやってくれます。事前に定義する必要はありません(定義することはできます)。しかも foo.bar にアクセスするとき、いつも同じオブジェクトが返されます。


>>> import mock
>>> x = mock.Mock()
>>> x.foo
>>> x.foo
>>> x.foo == x.foo
True

Mock インスタンスは常に呼び出し可能です。

>>> y = mock.Mock()
>>> y()

>>> y.hoge()


テストを書いてみる


get_user_timeline 関数の仕事は、(1) 引数 user を twitter.get_timeline() に渡すこと、(2) その戻り値から辞書を作成すること、です。user オブジェクトの作成や get_timeline() で何が起こっているかは、別の単体テストでやることです。なので、 user と get_timeline はモックにしちゃいましょう。

任意のモジュール内の、任意のクラスのメソッドを入れ替えるときは、patch 関数を使います。

class GetUserTimelineTest(unittest.TestCase):
    def test(self):
        # モックのコンテクストで実行
        # 動作を変えたいメソッドを文字列で指定する。
        with mock.patch('mymodule.Twitter.get_timeline') as m:
            # レスポンスで使うダミーの tweet を作る
            tweet0 = mock.Mock()
            tweet1 = mock.Mock()
            # get_timelineメソッドを呼び出したときの戻り値をモック
            m.return_value = {"tweets":[tweet0, tweet1]}
           
            # 引数で渡すオブジェクトもモックにする
            user = mock.Mock()

            # 単体テスト対象関数呼び出し
            result = get_user_timeline(user)

            # モックが1度呼ばれていることを確認
            self.assertEqual(m.call_count, 1)
            # モックが呼ばれたときの引数を確認
            self.assertEqual(m.call_args,
                             ((user.id, user.access_token),{}))
            # 戻り値の確認
            self.assertEqual(len(result), 2)
            self.assertEqual(result[0]['text'], tweet0.text)


わかりにくいなぁ。もっと時間かけて丁寧に書かないと、知らない人には伝えられない気がしてきた。