Python版mechanizeの関わるテスト

テスト始めました

最近, テストをちゃんと開発工程に入れようとし始めました. まだ手探り状態なんですけど.
とりあえず, nosetestsを利用してテストを行おうと思っています参考になったページは

pdb-failuresオプションは使おうと思ってるけど, まだあんまり使ってないです.

同時にPylintも試験導入中. --generate-rcfileで設定ファイルを作れるので, それを~/.pylintrcに配置してカスタマイズすると便利. 現在デフォルトからずらしているのは,

disable=I0011 #特定のエラーを消す
reports=no #統計レポートとか出さない
include-ids=yes #エラーIDを出す
output-format=colorized #色つきで表示

なお, 特定の場所のエラーを消すには,

class ProducerStatus:
    # pylint:disable=R0902,R0913,R0903

みたいな感じ.

mechanize(Python)のテスト

やっと本題. mechanizeというものを使うと, Pythonから簡単にウェブページにアクセス出来る. Cookieを持ってくれたり, ちゃんとRefererを扱ってくれたりする所が魅力.

urllib2をベースにしているっぽいので, 足りないドキュメントはある程度urllib2のドキュメントを読めば解決できるかも. mechanize.Requestとurllib2.Requestに共通するattributeは多い. また, ソースにはきっちりコメントを打ってくれているため, pydocを介してちゃんと情報を取ることが出来る.

さて, 今mechanizeが特定のページから目的のフォームを検出出来るか確かめたいとする. 例えば, Googleのページからちゃんと検索ボタンを探せるかとか. この場合, 実際のGoogleのページにアクセスするコードを書くのは簡単だが, Googleには迷惑だしネットワークに問題があった場合きちんと動かないという問題もある.

考えられる単純な方法はGoogleのふりをしたサーバーをローカルに立てることだ. ただ, これは結構めんどくさいし, やり方によってはおそらく副作用もある.

そこで, mechanizeにだけ見える偽のGoogleページを用意してやる. やり方はmechanizeのテスト(test_browser.py)を参考にすれば大体わかる.

まず, mechanize.Browserのインスタンスを作成する前に,

mechanize.Browser.default_features =
mechanize.Browser.default_others =

mechanize.Browser.default_schemes = []

としてやる必要がある. なんだろうこれ. 他のHandlerが動かないようにするのかな. ちょっとよくわからない. たぶん, mechanizeの設計思想として, Request, ResponseそれぞれにHandlerが順番に干渉していくと思うんだけど, ちゃんと読んでないので不明. なお, mechanizeのtest_browser.pyではclass TestBrowser(mechanize.Browser)としてTestBrowserのインスタンスを作ってる. でも私はテスト対象のコードをいじりたくないのでこんな感じで.

以下がテストコードの全文.

from test_browser import MockResponse,make_mock_handler,TestBrowser

def test_parse_work_page(self):
  mechanize.Browser.default_features = []
  mechanize.Browser.default_others = []
  mechanize.Browser.default_schemes = []

  br = MyBrowser()
  browser = br.browser_ #MyBrowserの中のmechanize.Browser型のメンバ

  url = "http://www.example.com/" #DummyのURL
  content = open("work_last.html").read()

  r = MockResponse(url,content, {"content-type": "text/html"})
  browser.add_handler(make_mock_handler()([("http_open", r)]))
        
  #実際にテストしたい関数. 目的のformを取り出す
  form = br.parse_work_page(mechanize.Request(url))

  answer = 'http://example.com/post_target'
  assert form.attrs["action"] == answer
  assert form.attrs["method"] == "post"

MockResponseなどはmechanizeのソースからもらってきている. これは, コンストラクタに与えられたurl, コンテンツ, ヘッダを持つページにアクセスしたかのように振る舞う偽のResponseである.

そして, make_mock_handlerで"http_open"というMockResponse rを返すメソッドを持つハンドラを定義している. そしてそれをBrowserに登録している. これにより, どのページにアクセスしても常にMockResponse rがレスポンスとして返ってくるようになる.

おそらくmechanize.Browserの仕様では, httpから始まるURLのページにアクセスすると, ハンドラの中からhttp_openというメソッドを持つものを検索し, それにRequestを渡すのだろう. 実際に, MockHandler.http_open(*args)を呼ぶと, MockHandler.handle("http_open", MockResponse r,*args)が呼ばれる様な仕組みになっている.


これでBrowserはどのページにアクセスしてもMockResponse rを受け取るようになったので, 実際のページのローカルコピーにアクセスさせ, 実際に目的のフォームを検出できているか確かめればよい.

ちょっと注意

Browserがhtmlのmeta情報を扱えるようにオプションをつけるとこのテストは動かなくなる.

# htmlのmeta情報を扱えるようになるオプション. Responseがメタ情報を含むようになる
#(これをつけるとデフォルトのMockHeaderで落ちるようになる)
browser.set_handle_equiv(True)

これは, 標準の偽ResponseヘッダであるMockHeaderが貧弱すぎて, メタ情報を格納するattributeが無いためである. この場合は, MockHeadersを拡張して

class MockHeaders(dict):
    def __init__(self,*args):
        """
        browser.set_handle_equiv(True)
        に対応するためにいくつか足した
        """
        dict.__init__(self,*args)
        self.dict = {}
        self.headers = []
    
    def getheaders(self, name):
        name = name.lower()
        return [v for k, v in self.iteritems() if name == k.lower()]

そのようなattributeを加えてやればよい.