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 温泉に行けました。順番がずれていますが、自分の人生で計画通りにいったことなどないので、自分にしては上出来です。