修行の場

知人が言っていました。これは修行だと。

scrapyのテスト方法を真面目に考える

背景

クローラが複雑になるのと、対応するWebサイトを増やしていくに連れて、 手動のテストでは厳しくなってきた。

Scrapyのテスト方法を考えました。 次のようなルールで行けば効率よくテストできそうです。 Scrapyはコンポーネント間の協調の仕方や協調のタイミングの詳細があまり乗っていないので、 複雑な機能をオフにして単純な機能から使うのが良さそうです。(これはフレームワーク全般に言えるかもしれませんが) 例えば、spidarsのallowed_domains設定がどこで使われるかわかりません。 これはOffsiteMiddlewareで使われていますが、spidarのinitで設定を変える方法などわかりません。 (おそらくできない)

  • middlewareはrequestやresponseのみに依存するものだけ利用。spidarのクラス変数を使った設定はしない。
  • クラス変数を使った設定が必要なら、クラスとして作りSpidarないから使う。
  • ユニットテストでspidarのprocessメソッドを呼び出し、戻り値を分類してテストする。
  • コマンドから与える引数は、Spidar.__init__の引数になるので、ユニットテスト内で与える
  • spidarないで使えるツール。LinkExtractorなどは積極的に使う
  • (理想を言えば、テスト用のWebサイトをローカルに保存してローカルサーバーから取るようにしたい。HTMLだけあれば良いはずなのでできそう)

初期設定

以下、ここに落ち着いた理由です。 Scrapyで使えるテストの選択肢としては、 次の方法を検討していました。

  • Scrapyで用意されたテストライブラリを使う
  • parseとか簡単なメソッドだけユニットテストしてあとは結合テスト
  • コマンド実行して何かを確認するテストを書く
  • 自分でコンポーネント間のデータやり取りをするライブラリを書いてテストする

Scrapyで用意されたテストライブラリを使う

コメントに仕様を書いておくと、 自動的にテストしてくれる仕組みがあるようです。 doctestよりも簡易化したもので、 contractを明治刷る仕組みがあり、それを書くだけでテストをしてくれます。 ただ、引数のバリエーションには対応できないので、 引数にバリエーションを持たせない開発の仕方をしている人しか使えません。

他にRun scrapy from a scriptなどコマンドからクローラーを動かす方法はあります。 スケジューラーの状態にアクセスする方法が明治されておらず、テストには使えなさそうです。 twistedの基本概念を知るのはテストには必要そう。

ユニットテスト

parseメソッドならモックを作って簡単にテストできます。 reponseモックを作って与えるだけです。 なので、この入出力をテストして、 パイプラインもモックテストするのが楽な方法だと思います。 小規模なプロジェクトならこれで十分カバーできそうです。

コマンド呼び出しして出力をテストする完全な結合テスト

これは方法が思いつきませんでした。

コンポーネントをつなぐ仕組みを自分で作る

プロジェクト設定の読み込みもできるそうなので、案外できるかもしれません。 本当は途中のデータをフックして、チェックできるような仕組みがあれば楽かもしれません。 ログだけではなく。 もしくはログとか出力する標準の機能を使って自動テスト書けるかもしれませんが、 簡単な方法はなさそうです。

    spider = TestTargetSpider(100, 200)
    requests = spider.start_requests()
    settings = {
        "DOWNLOADER_HTTPCLIENTFACTORY": "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory",
        "DOWNLOADER_CLIENTCONTEXTFACTORY": "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"
    }
    request = next(requests)
    handler = HttpDownloadHandler(settings)
    d = handler.download_request(request, spider)
    class Catcher:
        def store_response(self, res):
            self.response = res
            reactor.stop()
    catcher = Catcher()
    d.addCallback(catcher.store_response)
    reactor.run()
    print(catcher.response)
    parsed_things = spider.parse(catcher.response)

    # ....assertion