クローラーのテストについてのノウハウメモ

4 min read読了の目安(約4400字

こんにちは!
LAPRAS でクローラーの開発をしています @Chanmoro です!

気づけば12月になりアドベントカレンダーの季節になったので初めて Zenn で投稿してみます。
LAPRAS の愉快な仲間の記事もぜひみてくださいねー!

https://qiita.com/advent-calendar/2020/lapras

クローラーのテストについて

クローラーエンジニアとしてこれまでいくつかのクローラーを開発してきたのですが、クローラーの開発について相談をいただく機会もちょこちょこあり、その中で 「クローラーって何をどうテストしたらいいの?」 というのを聞かれます。
この記事ではこれまでのクローラーの開発経験の中でどのようなテストを構築したかについて簡単に書きます。

クローラーで意識するべきテストの種類

Unit test

どのようなコンポーネントを分けて設計するか、というのはここでは詳細を書きませんが(別途記事書きます)コンポーネント単位でのユニットテストはクローラーだからといって書きにくいということはなく、一般的なソフトウェアと同様にテストを書くことができます。

例えば、HTML をパースして構造化したデータに変換する責務を持つコンポーネントを作るとします。(僕はほぼ確実にそういうコンポーネントを作ります)
このコンポーネントは以下の PageParser のような処理になります。

@dataclass
class PageData:
    title: str
    description: str

@dataclass
class PageParser:
    html: str
    
    def parse(self) -> PageData:
        return PageData(
	    # パースの処理は Beautiful Soup とか lxml で実装することになる
            title="Somehow parse title from self.html",
            description="Somehow parse description from self.html",
        )

この PageParser に対するテストは以下のように書くことができます。
パース対象の HTML は事前にテストデータとして用意しておき、このコンポーネントへの入力を固定することがポイントです。

class PageParserTest(TestCase):
    def test_parse(self):
        with open(f"{os.path.dirname(__file__)}/data/test.html") as f:
            test_html = f.read()
        parser = PageParser(html=test_html)
        parsed_data = parser.parse()

        self.assertEqual(parsed_data.title, "title of html")
        self.assertEqual(parsed_data.descriptionn, "description of html")

このテストをパスしている限り想定された HTML に対するパース処理が壊れていない ことが保証されます。

この時「クロール先の HTML が変更された場合はどうするの?」という疑問が浮かびますが、ユニットテストではそのことは考えません。

Unit test が失敗するのはどんな時か

ユニットテストが必要なとき、つまりユニットテストが失敗する可能性のある場合はどんな時でしょうか?
これは自分たちが対象のコンポーネントのコードを変更する時は常にテストを失敗させる変更を入れてしまう可能性があります。
特にリファクタリングをする際にはテストを足場として既存コードを壊していないことを確認して作業する必要があるのでユニットテストによる動作の保証はとっても重要です。

Unit test の実行タイミング

コードが変更された都度テストが実行されるのが理想的なので、リポジトリに push したタイミングやビルド実行をトリガーとしてテストが実行されるようにしておくのがいいでしょう。
それ以外にも、今回紹介したようなテストコードを書くことによってクローラーであっても TDD で開発することができるようになるので、TDD で開発している場合はローカル環境でテストを頻繁に実行することになります。

Contract test

さて、先ほどの 「クロール先の HTML が変更された場合はどうするの?」 という疑問を解決するためにはどうしたらいいでしょうか?

このような「連携先システムが想定通りのインターフェースで動いているか?」をチェックするためのテストは一般的なソフトウェアテストの種類に定義されている「Contract test」という種類のテストと捉えることができます。
具体的なテストコードを見てみましょう。

class PageContractTest(TestCase):
    def test_parse(self):
        response = requests.get("http://<url of contract test target>")
        self.assertEqual(response.status_code, 200)

        parser = PageParser(html=response.text)
        parsed_data = parser.parse()

        self.assertEqual(parsed_data.title, "title of html")
        self.assertEqual(parsed_data.descriptionn, "description of html")

パッと見では先ほどのテストとあまり変わりませんね?
ユニットテストとの違いは実際に対抗先のサービスに接続している点です。
このテストでは実際にクロール先のサービスに HTTP リクエストを送りそのレスポンスに対してテストを書いています。

Contract test が失敗するのはどんな時か

Contract test が失敗する可能性のある場合はどんな時でしょうか?
これはそもそもテストしたかった対象の通りで「クロール先の HTML 構造が変化した時」にこのテストが失敗することになります。そして テストが失敗するトリガーになる変更はクローラー自体のコードの変更ではなく、クロール先のサービスの変更 ということになります。

Contract test が失敗するのは大抵以下の場合です。

  • クロール先サービスの HTML 構造が変化した時
  • サーバーエラーなど一時的に問題が発生している時

もしクロール先の HTML 構造が変化した場合であればクローラーの処理を修正する必要があります。
この時、先ほど紹介したような Unit test がある場合はテストデータの HTML も同時に最新の内容に更新することになります。

Contract test の実行タイミング

当然ながらクロール先のサービスがどんな頻度でデプロイされているかは僕らはわかりませんし、今後いつどういう変更がされるかというのは全くわかりません。

なので Contract test の実行はコードの push やデプロイのタイミングには依存せず、こちらの任意のタイミングで定期的に実行することになります。
実際のサービスにアクセスするため高頻度でテストは実行せずに、1日1〜数回程度で決まった時間に実行することが多いです。
(これを書いてて気付きましたが定刻よりもランダムな時間に実行してもいいかもしれませんね)

CircleCI や GitHub actions では cron のようにジョブのスケジュール実行を設定することができるのでそういった機能を利用してもいいですし、定期ジョブを実行できるジョブスケジューラーを使っても問題ないです。

まとめ

今回はクローラー開発で意識したいテストについて簡単にご紹介しました。

僕自身「Contract test」という言葉はこのスライドで使われているのを見て初めて知りました。このスライドはマイクロサービスのテストについての話なので文脈はちょっと違いますが、おかげでこの種類のテストを言語化することができました。

https://martinfowler.com/articles/microservice-testing/#testing-progress-3

Python のクローラーのフレームワークである Scrapy では $ scrapy check というクローラーのテスト用コマンドが用意されているのですが、この時実行されるテストケースは contract という名前のモジュールに基底クラスが配置されています。
最初これを見たときになぜ contract なのかわからなかったのですが、 Contract test という言葉を知った時に「あの contract は Contract test から来てるものだったのか!」と理解できました。

クローラーは自分たちでコントロールできない要素への依存が強いので「テストを書いても意味ない!」とか「テスト書きにくい!」と思うかもしれませんが、クローラーを構成するコンポーネントとそれらの境界を適切に設計することで、通常のソフトウェアと同様にテストを書いて堅く開発することが十分にできます。

クローラーの品質でお悩みがあればぜひ参考にしてみてください!

宣伝

さて、今回紹介したようなテストを書き開発したクローラーによってインターネット上のアウトプットから自分のプロフィールを自動構築する LAPRAS というサービスを開発しています!
ぜひ登録してみてくださいね〜!

https://lapras.com/