2011-11-28

mock.patch


mock ライブラリの patch 関数の挙動を書きます。

パッチャーの start()/stop() メソッドを使う

標準ライブラリの random.random 関数を例にとります。

>>> import random
>>> random.random()
0.90675850364670885
>>> random.random()
0.9838226858480108


patch 関数を実行してみましょう。

>>> from mock import patch
>>> p = patch('random.random')
>>> random.random()
0.27552766919082217

とくに、何も変わったことは起こりません。patch 関数の戻り値は _patch オブジェクトです。ドキュメントにはパッチャーと書いてあります。パッチャーの start() メソッドを呼ぶと変化が起こります。

>>> m = p.start()
>>> m
<mock.Mock object at 0x366f30>
>>> random.random
<mock.Mock object at 0x366f30>

random.random という名前の参照先が、元の乱数生成関数ではなく、Mock オブジェクトに置き換わっています。random.random と p.start() の戻り値は同一の Mock オブジェクトです。

random.random は Mock オブジェクトなので戻り値を書き換えることもできます。

>>> random.random() >>> m.return_value = 100 >>> random.random() 100 >>> random.random.return_value = 0 >>> m() 0 >>> random.random() 0

パッチャーには stop() メソッドがあり、これを実行すると元に戻ります。

>>> p.stop()
>>> random.random
<built-in method random of Random object at 0x6b3b6210>
>>> random.random()
0.029689653478273681

テストで使う

def foo(x):
    return random.random() * x


関数 foo をテストする場合を考えます。テストすべきは、 

  1. random.random を引数なしで 1 度呼び出したこと。
  2. random.random() の戻り値に、x をかけたものが返ること。
です。random 関数の戻り値がわかっていれば、テストできますね。モックしましょう。

import random
import unittest
import mock

def foo(x):
    return random.random() * x

class MyTestCase(unittest.TestCase):
    def test(self):
        # random.random が常に1を返すようモックる
        p = mock.patch('random.random')
        p.start()
        random.random.return_value = 1
        # テスト対象関数を呼び出す
        result = foo(2)
        # random.random() を1度呼んでいることを確認
        self.assertEqual(random.random.call_count, 1)
        self.assertEqual(random.random.call_args, ((), {}))
        # 戻り値をテスト
        self.assertEqual(result, 2)
        # モックを戻す
        p.stop()  

if __name__ == '__main__':
    unittest.main()


これでテストできるようになりました。ですが、問題がありまして、テスト中に例外が出ると p.stop() が呼ばれません。random.random はモックのままなので、他のテストが実行されたきに、意図しない結果になることもあります。

というわけで、必ず実行されるように、setUp と tearDown で、パッチャーの start() と stop() 使います。

class MyTestCase(unittest.TestCase):
    def setUp(self):
        # random.random が常に1を返すようモックる
        self.p = mock.patch('random.random')
        self.p.start()
        random.random.return_value = 1

    def tearDown(self):
        # モックを戻す
        self.p.stop()

    def test(self):
        # テスト対象関数を呼び出す
        result = foo(2)
        # random.random() を1度呼んでいることを確認
        self.assertEqual(random.random.call_count, 1)
        self.assertEqual(random.random.call_args, ((), {}))
        # 戻り値をテスト
        self.assertEqual(result, 2)

つづき

patch をデコレータや、コンテクストマネージャとして使えるのですが、それは、また今度。

2011-11-25

mock ライブラリの Mock クラス

mock ライブラリを使ったテストのことを書こうと思ったのだけれど、テストの仕方なんて教えてもらう立場なんだから、いい例を思いつくわけもなく、挫折。

というわけで、mock ライブラリで提供されている機能を、簡単に書きだしていくことにします。mock を使ってテストする具体的な方法は、また今度。

今回は Mock クラスの基本機能です。ちなみにこれだけでは、テストを書くにあたってのメリットは限定的です。

Mock クラスは引数なしで、インスタンス化できます。以後、Mock オブジェクトと呼びます。

>>> from mock import Mock
>>> wozozo = Mock()
>>> wozozo
<mock.Mock object at 0x106fc4390>

Mock オブジェクトにドットでつなげると、別の Mock オブジェクトが取り出せます。

>>> wozozo.foo
<Mock name='mock.foo' id='4412162832'>
>>> wozozo.bar
<Mock name='mock.bar' id='4412163024'>
>>> wozozo.bar
<Mock name='mock.bar' id='4412163024'>

同じプロパティ名を指定すると、同じ Mock オブジェクトが返ってきます。上の例では、wozozo.bar は必ず同じ Mock オブジェクトを参照しています。

もちろん、任意の属性に、任意のオブジェクトを代入できます。

>>> wozozo.baz = 3
>>> wozozo.baz
3

Mock オブジェクトを、関数のように呼び出すこともできます。また、 call_count プロパティは呼び出した回数を返します。

>>> wozozo()
<mock.Mock object at 0x106fc44d0>
>>> wozozo.call_count
1
>>> wozozo()
<mock.Mock object at 0x106fc44d0>
>>> wozozo.call_count
2

Mock オブジェクトなら呼び出せるので、ドットでつないで得られるプロパティも呼び出せます。引数を渡すこともできます。

>>> wozozo.unko(1)
<mock.Mock object at 0x106fc4590>

call_count の他にも呼び出し系のプロパティがあります。call_args は、最後に呼び出されたときの引数を返します。また、call_args_list は呼び出されたときの引数の履歴を返します。

>>> wozozo.unko.call_count
1
>>> wozozo.unko.call_args
((1,), {})
>>> wozozo.unko(1, 'a', foo='hoge')
<mock.Mock object at 0x106fc4590>
>>> wozozo.unko.call_count
2
>>> wozozo.unko.call_args
((1, 'a'), {'foo': 'hoge'})
>>> wozozo.unko.call_args_list
[((1,), {}), ((1, 'a'), {'foo': 'hoge'})]

戻り値を指定することもできます。下の例では、wozozo.unko() で 999 を返すようにしています。

>>> wozozo.unko.return_value = 999
>>> wozozo.unko()
999

任意の関数を割り当てることもできます。

>>> def f(x, y):
...     return x + y
... 
>>> wozozo.unko = f
>>> wozozo.unko(2, 3)
5

てな具合です。

何が嬉しいかと言うと、簡単に何かのフリをさせることができる、ということです。すぐに思いつくのは (1) 生成するのがだるいインスタンスのフリをさせる、 (2) 関数やメソッドのフリをさせる、の2つです。

1つめの例として datetime オブジェクトを受け取って、年の情報だけを使うような関数を考えます。

>>> def next_year(today):
...     return today.year + 1
... 
>>> next_year(datetime.now())
2012
>>> today = Mock()
>>> today.year = 2000
>>> next_year(today)
2001
datetime なら、年月日を直接指定できるので大したことないですが、複雑なプロセスを経て、他のオブジェクトへの参照をしているようなオブジェクトで、かつ、一部のプロパティしか使わないのであれば、 Mock オブジェクトで代替すると便利です。

2つめの例としては random 関数を考えます。戻り値が決定論的に定まらないので、テストするのが大変です。そこで random の名前のくせに、常に0.5を返すようにしてみます。

>>> random = Mock()
>>> random.random.return_value = 0.5
>>> random.random()
0.5
>>> random.random()
0.5
>>> random.random()
0.5

こうすると、テストがやりやすくなりますね。

問題は、何かの関数の「中で」random 関数が呼び出される、ということです。もちろん、random 関数を引数に与えるような設計にするといいのでしょうが、人からもらったコードはそうなってないこともあるでしょう。

それを、うまくやってくれるのが mock.patch() 関数です。

眠い。寝よ寝よ。つづきは、またこんど。

2011-11-24

Python 温泉で開発プロセスの教えを乞う


初めて参加した Python 温泉で、 @voluntas と @aohta に開発プロセスの教えを乞いました。いろいろ教えてもらった中で、実際に手を動かし始めたことを書きます。@voluntas のブログ記事をベースに書いていますが、ふたりに教えてもらったことを混ぜています。

前提は、
- 自社サービス開発
- エンジニアの人数は不足気味
- 使用する言語は Python のみ
- ウェブ API 開発がメイン

環境の構築
開発環境を簡単に作れるというのは実はとても重要なファクターです。
これを目指すのがオススメです。git clone | hg clone して make だけたたけばあとは全部用意してくれるが理想ですね。

これは私が苦手なこと(そういうものが私には多い)のひとつです。というわけで、本当に基本的なことだけやりました。

人生初の buildout を使いました。いままで、便利そうなんだけど、よく分かんないってことで敬遠していました。が、今回は 「Python ライブラリを入れる」ことだけを書きました。

Makefile
.PHONY: env

env:
     python2.7 bootstrap.py --distribute
     bin/buildout

buildout.cfg
[buildout]
parts = env

[env]
recipe = zc.recipe.egg
eggs =
    nose==1.1.2
    mechanize==0.2.5
    simplejson==2.2.1
interpreter = python


これだけ。これで hg clone して make したら環境をつくれます。なんで今までやらなかったんだろう。無用な混乱を避けるためにバージョン番号を指定してしてあります。
単純だけど動く buildout.cfg を書けるようになったことが、今回のツール知識の中で最大の成果です。

機能テスト


上の環境は、開発しているアプリケーション本体ではなくて、その機能をテストするためのものです。@voluntas のブログでいうところの外部テストです。なので、nose が入ってるんですね。アプリケーションは HTTP で JSON を返すので、戻り値のチェックのために simplejson を入れています。
import urllib
import simplejson as json

def setup():
    # いろいろ初期化
    __SERVER = …
    ...

def test_my_api():
    res = app.call_api('/my/api', x=1, y=2)
    assert res['status'] == 0

def call_api(path, **kw):
    """API を呼び出して、レスポンスを辞書で返す"""
    params = urllib.urlencode(kw)
    fin = urllib.urlopen('%s%s' % (__SERVER, path), params)
    body = fin.read()
    return = json.loads(body)
上のようなファイルを作っておいて、 bin/nosetest を実行すると、setup して test_my_api を実行してくれます。 nose を使うのも初めてですが簡単でした。

自社ライブラリ


前述の call_api 関数は別のモジュールに切り出してあります。ゆくゆくは自社ライブラリとして格上げの予定。
大したコードではないので自社ライブラリにしなくていいんじゃ、と思ったのです。が、「call_api のテストだけ書いておけば、[simplejson などの] 依存先の使わない機能の不具合を無視できるのだから、自社ライブラリの動作確認だけすればよいでしょ。だから自社ライブラリにしちゃえ」という教えでした。これが一番私にとって重要な考え方でした。

次の課題


buildout でインストールした mechanize は OAuth 機能のテストに使います。リダイレクトやブラウザ上で操作などがあるので、自動テストにはブラウザを抽象化してくれるライブラリが必要でした。

def signup_with_twitter(screen_name, password):
    """Twitter アカウントでログイン"""
    def userop(browser):
        # Twitter にログインして承認
        browser.select_form(nr=0)
        browser["session[username_or_email]"] = screen_name
        browser["session[password]"] = password
        browser.submit()
    return _oauth('twitter', userop)

def _oauth(authority, userop):
    browser = mechanize.Browser()
    browser.set_handle_robots(False)
    # OAuth 開始の URL を開く
    browser.open("%s/oauth?authority=%s" % (__SERVER, authority))
    # ユーザ操作
    userop(browser)
    # レスポンスパラメータを取得
    # example.com/path?foo=1&bar=x => {"foo":["1"], "bar":["x"]}
    url = browser.geturl()
    data = cgi.parse_qs(urlparse.urlparse(url).query)
    return data


これはこれで Twitter を使った OAuth のテストができるので問題ありません。問題は、Twitter に問題があると、テストが FAILすることです。それは Twitter の問題であって、私が開発しているアプリケーションの問題ではありません。
なので、OAuth が成功した or 失敗したふりをしてくれるモックが必要です。それが次の課題だろうな、と考えています。


おわりに


何をしようとしているかというと、継続的インテグレーションをしたいのです。サーバサイドのエンジニアが少ないので、できるだけ自動化したいわけです。人間は創造的なことに時間を使うべきだ、と個人的に信じています(それでまあ、例の対談とかの流れになるわけです)。そのためには自動的に環境構築して、テストできるようにする必要があるのですが、ずーっと止まっていました。今回の温泉で、一歩目を踏み出せたのは大きな収穫でした。

おまけ


35歳ごろ(定年だというのに)ソフトウェア開発業界に入りたい、と思うようになりました。このとき、Python Code Reading で発表 → BPStudy に行く → Python 温泉に行く → 顔を知ってもらう → どっかの会社に潜り込む、という戦術を妄想していました。そして、37歳にしてやっと Python 温泉に行けました。順番がずれていますが、自分の人生で計画通りにいったことなどないので、自分にしては上出来です。

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)


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