@IndeedEng テックトーク 2014年を振り返って

「We Help People Get Jobs (人々の仕事探しのお手伝いをする)」 — これが私たちのミッションです。

世界ナンバーワンの求人検索サイトとして、多くの求職者に最高のユーザー・エクスペリエンスを提供する ― Indeed では、エンジニア一人一人がこの課題に取り組んでいます。

当ブログを2012年に開設した際、私たちは自らの経験から学んできたことを共有し、広くソフトウェアコミュニティに貢献することを目標に掲げました。

Indeedが主催する@IndeedEng テックトークの評判は上々で、2014年のテックトークシリーズにはのべ700名以上の参加者が集まりました。さらに当ブログには、128ヶ国から59,000件ものアクセスがありました。このように、私たちの発信するユニークな情報への反響は大きくなってきています。

今回は、2014年を振り返って、私たちが共有したコンテンツを簡単に紹介したいと思います。

まず、 Indeed が開発においてもっとも大事にしていることは、あらゆるものをテストし、結果を測定するというプロセスです。このモットーを実現するために、私たちはA/Bテスト・フレームワークである自社の Proctor を使用しています。

私たちはその Proctorを2013年にオープンソース化し、2014年には  Proctor の機能とツールや、 Indeed の開発における Proctor の活用法をブログで公開しました。

また、Proctor をリモートサービスとしてデプロイできる Java のWeb アプリ、proctor-pipet もリリースしました。

さらに、私たちは util-urlparsing のオープンソース化も行いました。 これは自社開発した Java ライブラリで、不要な中間オブジェクトを作成せずに、 URL クエリ・ストリングをパースすることができます。

2014年前半には、Imhotep を 特集したテックトークを数回開催しました。

Imhotep は Indeed の インタラクティブなデータ分析プラットフォームであり、これによって私たちはデータに基づく意思決定を行います。 決定木の拡張、 大規模分析ツールの構築、そして 主要なメトリックにフォーカスするための Imhotep 使用法、などのトークでは、その技術について語っています。

11月には、私たちはこのプラットフォームを部分的にオープンソース化しました。それに伴い、 Imhotep を特集したテックトークとワークショップを開催し、参加者には自身のデータを使い Imhotep を体験してもらいました。

グローバル企業である、という挑戦に私たちは真剣に向き合っています。

新規マーケット開拓を通した私たちの経験や、グローバルマーケットでの成功に必要な、様々な技術的問題への取組みを皆さんにも共有しています。

今後このブログやテックトークでは、日本語版のコンテンツの登場など、新たな試みに取り組んでいく予定です。 これから数か月に渡り、どんどん日本語版ブログの投稿もアップしていきますので、ぜひご期待ください。

2014年に、私たちがこんなに多岐にわたるアクティビティをお届けできたのも、皆さんのおかげです。

ブログのフォロワーの皆さん、@Indeedテックトーク視聴者の皆さん、応援ありがとうございます! Indeed は、2015年も様々な媒体を通して皆さんと交流できることを楽しみにしています。

僕がユニットテストをする理由

もし君が、この 15 年間に少しでもソフトウェア開発に携わったことがあるなら、ユニットテストの重要性を誰かがくどくど言うのを、きっと聞いたことがあるはずだ。

マネージャーからこんなセリフを聞いたことはないだろうか。

「ユニットテストって最高じゃないか!コードを文書化してくれるし、バグの混入リスクも減る。変更にかかるコストとリスクも減らせるから作業スピードを落とさないで済む。良いユニットテストは、開発からデプロイまで全体のスピードを上げてくれるよ!」なんて。

確かにこれらはユニットテストをするにあたって、妥当な理由であるけれど、それはあくまでマネジメント上の理由だ。

僕もそういった理由には賛成だ。けれど今述べた理由は、開発者として僕がユニットテストをする理由の本心とは違う。

僕がユニットテストをする理由はとてもシンプルだ。

ユニットテストは、新旧のデザインの向上や、ソフトウェア・デザイナーとしての腕を上げるための絶好のチャンスであり、強いモチベーションの元になる。

コツは、出来るだけ書くユニットテストの数を少なくし、それぞれのテストがシンプルになるようにすること。

これが何故うまく行くか?理由は、シンプルなユニットテストを書くことは、本質的に退屈で、コードが酷ければ酷いほど、テストするのもより難しく退屈になるからだ。

ユニットテストを動かす唯一の方法は、必要なユニットテストが殆どないくらいに劇的に実装を改善して、それからテストを書くことだ。

実装を改善すれば、ユニットテストは減らせる

実際に作成するユニットテストを削減するには、以下のようなアプローチがあげられる。

  • 重複したコードのリファクタリング  もしコードブロックを抽象化できたら、その分ユニットテストの数が減る。
  • デッドコードの削除   削除できるコードにわざわざユニットテストを書く必要はない。こんなこと当たり前だと思ったら、レガシー・コード・ベースをもっと見てみて欲しい。その理由がわかるはずだ。
  • フレームワークのボイラープレートを、 Configuration か Annotation として外部化  そうすれば、 Scaffolding (スキャフォールディング)としてではなく、テストが必要なプロダクトにだけテストを書くことが出来る。
  • コードの各分岐は、最低 1 件のユニットテストが必要になる。だから if 文やループを削除できると、その分テストの件数も減らせるのだ。 実装言語によっては、派生型ポリモーフィズム、コードの移動、プラグイン可能な Strategy パターン、アスペクト指向、 Decorator パターン、高次のコンビネータ等のテクニックを取り入れることで if 文やループを削除できる。コード内に分岐点がある分だけ、追加のテストに依存してしまいかねない。出来るだけ減らそう。
  • もっと奥底にあるデータフローのパターンを見つけて、抽象化する 偶発的な計算処理を取り出すことで、似ていないように見えるコードも、似せることが出来る。一度これを行ってしまえば、基本構造を統合できる。そうすれば、簡単にテスト可能で分岐のないコードをどんどん増やせる。最終的には、シンプルなセマンティック・ルーチン(述語や シンプルなデータ変換が多い)と 10 個くらいの再利用可能なコントロールパターンの連なりが残るだろう。
  • 出来るだけ ビジネス上のロジック、永続性、プロセス間の通信を別々にすることでモックオブジェクトをいじくりまわすのを回避できる。 モックオブジェクトは「コードの臭い」なので、多用している場合、コードが結合しすぎているサインかもしれない。
  • ロジックを一般化する方法を見つける。そうすれば、エッジケースにもメインフローが適用され、多様で複雑なインプットもひとつのテストでカバーできる。特別なケースに対しては、つい他に使い回せないコードを書いてしまいがちだ。しかし、その代わりに特別な取扱いのいらない、もっと一般的なソリューションを探すことだって可能なはず。ただ、注意して欲しいことは、よりシンプルかつ一般的なソリューションを探す方が、特別なケースを沢山作成するよりも、ずっと難しい、ということ。シンプルなコードを少しだけ書くための時間が十分になく、その代わりに複雑なコードを大量に書かなければならない、といったこともあるかもしれない。
  • 既存ライブラリ内のメソッドとして既に実装されているロジックを認識して置き換える。そうすれば、ライブラリの作者にユニットテストを押しつけてしまうことが出来る。
  • もし、データオブジェクトをイミュータブルになるくらい単純化できて、オペレーションも簡単な代数的な規則に従うだけ、という所まで行くと、プロパティに基づいたテストを活用できる。これは文字通り、ユニットテストが自動で書き込みをしてくれる、というもの。

能書きはこれくらいにして、実際にコードを見てみよう。

深いパターンを見つけだし、重複したコードを抽象化する

データサイエンスのコードでよくあるパターンとして、ある関数が最適化されたコレクションの要素を探す、というものがある。これに対する最もシンプルな  Java コードは、以下の様なものになるかと思う。

  double bestValue = Double.MIN_VALUE;
  Job bestJob = null;
  for (Job job : jobs) {
    if (score(job) > bestValue) {
      bestJob = job;
    }
  }
  return bestJob;

このコードは、とても簡単で、考えなくても素早く書けてしまうかもしれない。ループと  if  文だけ!うまく行かないはずないって?

確かに、初めの数回は良いけれど、こういうコードを書く度に「技術的負債」が増えてしまう。ユニットテストを書くと、重複したコードとリスクが段々と見えてくる。コードの各ブロックがテストを必要とするのは、一般的なケースでの正しさを調べるためだけでなく、沢山のエッジケースを調べるためでもある。

でも、もし空のコレクションを渡してしまったら?単一要素のコレクションは? null は?

上のコードみたいにシンプルなコードでも、ユニットテストに引っかかるようなバグを持っていたりする。すると、最適化をしようとする度に、沢山テストを書かなくてはいけなくなる。

あくまで僕の意見だけれど、僕ならもっと別のことに自分の時間を使いたいね。

もっと良いソリューションとしては、こんなものがある。

まず、大事なことに気づいて欲しい。

それは、小さな量の重複コードでも、抽象化、コード作成、テスト実行までの作業は一度きりに出来るし、一度きりにすべきである、ということだ。

これは、コードを一般化してエッジケースの修正をするチャンスにもなる。

    public static <J> J argMax(Iterable<J> collection,
                               Function<J, Double> score) {
      double bestValue = Double.MIN_VALUE;
      J bestElement = null;
      if (collection != null) {
        for (J element : collection) {
          if (score.apply(element) > bestValue) {
            bestElement = element;
          }
        }
      }
      return bestElement;
    }

このコードはユニットテストを一度しか必要としない。

さらに改善するにはこのロジック全てを、ライブラリコールと置き換えればいい。

(以下の例はGoogle の Guava ライブラリ)

  public static <J> J argMax(Iterable<J> collection,
                             Function<J, Double> score) {
    return Ordering.natural().onResultOf(score).max(collection);
  }

その後は、使用するスコアリング関数ごとにユニットテストが必要になるだけ。

他は全て処理済み、となるのである。

ユニットテストを避ければ最高のソフトウェアデザインが見えてくる

この記事に書かれたユニットテスト回避のテクニックは全て、障害に強く柔軟なデザインに欠かすことの出来ないものだ。

これは、ユニットテストをやる・やらないに関わらず当てはまることだ。

しかし僕らはつい、物事を前に進めようと焦るあまり、これを忘れてしまいがちになる。けれど、ユニットテストを継続することで、本当に必要なユニットテストをする時間と理由が出来てくるはずだ。

そして、「ユニットテストをあまり実装しない」という、良い意味での手抜きをすることで、プロジェクトデザインと実装の継続した改善を行えるだろう。

少なくとも、やるかやらないかは君次第。

もし君が、コードの基本デザインを改善せずに、ユニットテストに当てるべき時間をテスト作成に当てているなら、そこから学ぶことは恐らく何もないだろう。

もしかしたら、そもそも「なんとなく動く」以上の質のコードを書く理由がないのかもしれない。

もし君が、プロダクトコードを改良することで、ユニットテストに当てるべき時間そのものが減るように努めているなら、「上質なデザインを持つソフトウェア」が一体どんなものか、きっとすぐに解るはずだ。

あくまで僕の意見だけれど、これだから僕はプログラミングが好きなんだ。

(了)

デイブ・グリフィスは Indeed のソフトウェアエンジニアで、ソフトウェアシステムの開発に 20 年以上携わっています。

A/B テストにおける Indeed の Proctor 使用法

(編集者より: この記事は、  Indeed  が提供する、 A/B テストのオープンソース・フレームワークである Proctor についての連載シリーズの第 2 回となります。)

Indeed における Proctor

前回の記事では、 Proctor が提供する機能やツールについて説明した。

Proctor は Indeed が提供するオープンソースの A/B テスト・フレームワークである。

今回は補足として、 Proctor がどのように Indeed の開発プロセスに取り入れられているかを更に詳しく書いていく。

カスタマイズされた Proctor Web アプリ

Proctor Web アプリの Indeed 内部でのデプロイは、 Atlassian JIRA 、 SubversionGit 、そして Jenkins を取り入れている。

課題の関連づけや、様々なサニティ・チェック (正常に動作するかの確認) 、課題管理の自動化に、私たちは JIRA を使用している。

変更履歴の管理に使用するのは、 Subversion。

(これまでの経緯から Subversion を使用しているけれど、 Git も選択肢の 1 つだ。)

テスト・マトリックスのビルドには Jenkins を使用している。

Prcotor Web アプリは内部管理用のデータストアと統合され、どのアプリケーションで、どのバージョンのテストが使用されているかを表示してくれるのだ。

proctorwebapp-history

図 1 : アプリ内のテスト定義変更履歴のスクリーンショット

JIRA による課題管理

Indeed では、テスト定義への変更を含む全課題を、 JIRA で管理している。

新規テストのリクエストや既存テストへの変更は、 JIRA 内でカスタムの課題タイプとして表示される。これは ProTest と呼ばれている(Proctor Test の略)。

ProTest の課題は、テストが属するアプリケーション用の JIRA プロジェクトの中で管理されている。また、 ProTest の課題は、 Proctor Web アプリのデプロイに結び付けられたカスタムのワークフローを使用している。

アサインされた ProTest の課題を承認すると、課題を承認した人(オーナー)が Proctor Web アプリを使用して、テスト定義を変更する。

変更を保存すると、課題のオーナーは ProTest の課題のキーを提供する必要がある。 Proctor のテスト定義レポジトリにコミットする前に、 Web アプリはまず、その ProTest の課題が存在して、有効な状態である(例: クローズされていない等)ことを確認するのだ。

その後、コミットのメッセージ内のキーを参照しながら、 Web アプリは変更を(ログイン中のユーザーに代わって)コミットする。

課題のオーナーが ProTest の課題に対する変更を全て実行した後、JIRA のワークフローは通常以下の通りになる。

  1. 課題のオーナーが課題を解決し、ステータスは QA Ready ( QA に回す準備が完了) になる。
  2. リリース・マネージャーは Proctor Web アプリを使用して、 QA に新しい定義を渡す。Web アプリは、課題のステータスを In QA(QA 実行中) に進める。
  3. QA アナリストは QA 環境で予想されるテストの挙動を検証し、その後 Production Ready (本番に回す準備が完了) にステータスを進める。
  4. リリース・マネージャーは Proctor Web アプリを使用して新しい定義を本番環境に進ませる。そうすると、テスト変更は1、2分の間に世界中に配信され、有効となる。Web アプリは、課題のステータスを In Production (本番実行中) に進める。
  5. QA アナリストは本番環境で予想されるテストの挙動を検証し、課題のステータスを Pending Closure (クローズ待ち) に進める。
  6. 課題のオーナーは、課題を「解決」し、全作業が完了し、本番に移行したことを反映する。

有効なテストグループのサイズを調整するだけならば、 Proctor Web アプリはこのプロセスをスキップし、自動的に変更を本番へプッシュする。

QA チームがテストへの修正を検証するのは、これらの修正が、予期せぬ挙動や、他のテストとの連携に不具合を引き起こすことがあるからだ。

テスト定義内のルールは、デプロイ可能なコードの形になっており、正確さを常に保つ必要がある。

検証のステップは、修正が本番で有効になる前に最後にもう一度、予期せぬ影響を未然に防ぐ機会を、 QA アナリストに与えてくれるのだ。

以下のルールの場合は、米国とカナダ国内の英語ユーザーのみを対象にテストを利用可能にしようとしているものだ。

    (lang=='en' && country=='US') || country=='CA'

ただし、括弧が誤った場所にあるため、カナダ国内のフランス語を使用するユーザーにも、彼らにはまだ対応していない挙動が見えてしまう。開発者は、自分の望むグループに自身を強制的に割り当てている場合、このバグを見落としてしまっている可能性がある。

QA 中にバグをすぐに見つけることができれば、期待される挙動が本番へ到達しなかったことに気づくまでの時間を無駄にせずに済むのだ。

テスト定義ファイル

テスト定義は proctor-data と呼ばれる単一の共有プロジェクト・レポジトリに保存される。プロジェクトは 1 つのテスト定義に対して、 1 件のファイルを含んでいる。

test-definitions/<testName>/definition.json

ほとんどの場合、テストの修正は Proctor Web アプリ 経由で行われる。

これをうけて、定義ファイル内の JSON への変更を行い、(ログイン中のユーザーに代わって)これらの変更を、バージョンを管理するレポジトリに、コミットするのだ。

定義ファイルは proctor-data 内で qaproduction の2つのブランチに複製される。テスト定義の修正が QA に送られると、全てのテスト定義ファイルは qa ブランチにコピーされ、コミットされる。(単一のリビジョンに関連する diff を適用または「チェリーピック」するのではない。)

同様に、テスト定義の修正が本番に送られると、全ファイルは production ブランチにコピーされ、コミットされる。 1 つのテスト定義に対して、 1 件のファイルが存在するので、このシンプルなアプローチは、マージのコンフリクトを避け、どのトランク・リビジョンの差分をチェリーピックするか決定せずとも、 JSON 定義の整合性を保っている。

テスト・マトリックス― ビルドとデプロイ

Proctor はテスト定義ファイルのセットを単一のマトリックス・ファイルにまとめることができるビルダーを含んでいる。

同時に、テスト定義が内部で一貫していて、未定義のバケット値を参照せず、アロケーションの合計が 1.0 になることを、このビルダーは保証してくれる。

また、これは Java から直接呼び出すことも可能なほか、 Ant タスクMaven プラグインを介し呼び出すことも可能だ。

Indeedでは、 proctor-data プロジェクト内にある Ant を呼び出す Jenkins のジョブを使用して、 1 つのマトリックス・ファイルをビルドしている。 Maven を使用したビルドの例は、 GitHub から利用可能だ。

テスト変更がトランクにコミットされる度に、継続的インテグレーション (CI) ツールである Jenkins のジョブが、テスト・マトリックスをビルドしている。

そのマトリックス・ファイルは、 CI 環境のアプリケーションとサービスに利用可能だ。

リリース・マネージャーがテスト変更を QA に送ると、 QA 専用の Jenkins のジョブが qa ブランチを使用してテスト・マトリックスをビルドする。

生成されたマトリックス・ファイルは、その後で全 QA サーバーにパブリッシュされる。マトリックスをコンシュームするサービスとアプリケーションはそれを定期的にリロードする。

これに相当する本番用 Jenkins ジョブは、 production  ブランチにおける新規の変更を処理する。

アプリケーション内の Proctor

各プロジェクトの Proctor 仕様の JSON ファイルは各プロジェクトのソースコードと一緒に通常のパスに保存されている(例:src/main/resources/proctor)。

ビルドの際には、コード・ジェネレータを( Maven プラグイン または Ant タスク を経由して)呼び出し、コードを生成する。続いて、プロジェクトのソースコードと一緒にそのコードはビルドされる。

一般的に新しいテストをローンチする際には、テスト・マトリックスを、それに依存するアプリケーションコードより先にデプロイする。

しかし、もしアプリケーションコードが先にデプロイされると、 Proctor はフォールバックし、テストを無効なものとして扱う。これは、もし、無効なバケットの値を -1 にマップするという慣例にしたがっている場合の話である。

テスト仕様内で fallbackValue を望ましいバケット値に設定することで、フォールバックの挙動が変更可能になる。

テストグループとコントロールグループが予期せぬサイズに変わらないことを保証するために、私たちは、ログをとっていない無効なグループにフォールバックすることを慣例としているのだ。

仮に、月曜から木曜に実行されるテストに対し、グループ 0 (コントロール) と 1 (テスト) があるとして、グループ 0 にフォールバックが設定されているとする。

もし、このテスト・マトリックスが火曜の 2 pm から 5 pm の間、何らかの変更の結果、壊れていたとすると、月曜から木曜までの全期間におけるメトリクスの合計が、コントロールグループの結果をゆがませてしまうのだ。

もし、フォールバックが -1 (無効) に設定されていれば、コントロールおよびテストグループに対するゆがみは発生することはない。

テストに新規バケットを追加する際、通常以下のような一連のアクションを実行している。

  1. 新規バケットにアロケーションを持たないテスト・マトリックスをデプロイ。
  2. 新規バケットの存在を認識しているアプリケーションコードをデプロイ。
  3. そのバケットに対するアロケーションを持つマトリックスを再度デプロイ。

もし、新規バケットのアロケーションを持つマトリックスがデプロイされ、アプリケーションがその存在を認識していない場合、 Proctor は全てのケースでフォールバック値を使用し、問題が起きても、安全な範囲におさまるようにしてくれる。

Proctor をこういう風に作った理由は、ある場合において、一定期間にわたって不明なバケットを適用させる命令をアプリケーションにくださないようにするためである。これが、分析をゆがませる可能性があるからだ。

また、マトリックスから全テストを削除する際にも、同じような注意を払っている。 

テストグループに「所属していないか」ではなく、

「所属しているか」をテストする

Proctor のコード生成は、テストグループへの所属を調べるための、使いやすいメソッドを提供してくれる。

そしてこれらのメソッドは、テストグループに 「所属していないどうか」 をテストするよりも、 「所属しているかどうか」 をテストするために、いつも使用したほうが良いということが解ってきた。

「所属していない」 ということに基づいた条件付きのコードを書いた場合、予期せぬ状況でその条件付きの挙動が生じるおそれがあるのだ。

例えば、仮に [50% control, 50% test] のスプリットが存在するとして、コード内には条件付き表現 !groups.isControl() があるとする。これは groups.isTest() と同等だ。
そして、テストのフットプリントを減らすため、比較用に同サイズのコントロールグループは残しつつ、テストのスプリットを [25% control, 50% inactive, 25% test] に変更するとする。

すると、条件の表現が groups.isTest() || groups.IsInactive() に等しくなる。
このロジックはおそらく、コントロールグループと無効のグループでは同じ挙動を維持する、という最初の目的とは違っているだろう。
この例でいうと、もし最初から groups.isTest() を使用していれば、予期せぬ挙動を紛れ込ませてしまうことを防げたはずである

バケット・アロケーションの進化

テスト・バケットにユーザーを割り当てることが、彼らに対するサイトの挙動に影響を与えることを、私たちはよく知っている。

テストがどんどん進化していくのに、Proctor だけでは、連続したページビューや、サイトへのビジットに対して、一貫したユーザー体験を保証することはできないのである。

そしてアロケーションを増減させる際には、ユーザーへの影響を慎重に考える必要がある。

通常の場合、一度ユーザーが特定のバケットに割り当てられたら、そこに紐づく挙動を、テスト期間中継続してユーザーに見えるようにしたい、と私たちは考える。
もしアロケーションが [10% control, 10% test, 80% inactive] で開始した場合、 [50% control, 50% test] に引き上げると、最初にテスト・バケットにいたユーザーがコントロール・バケットに移動してしまうので、これは避けたいと思うだろう。

バケットを安定して大きくしていくのに、 2 つの戦略が使われている。

「スプリット・バケット」 と呼ばれる戦略 (図 2) では、無効の範囲から 40% の塊を 2 つ取り出して、 10/10 から 50/50 に移行し、既存のバケットのために新規範囲を追加する。

その方法で出来た JSON が図  3 である。

 

split-bucket

図 2 : 複数の範囲にバケットをスプリットし、コントロール/テストを拡大

 

  "allocations": [
  {
      "ranges": [
      {
          "length": 0.1,
          "bucketValue": 0
      },
      {
          "length": 0.1,
          "bucketValue": 1
      },
      {
          "length": 0.4,
          "bucketValue": 0
      },
      {
          "length": 0.4,
          "bucketValue": 1
      }
      ]
  }
  ]

図 3 : 「スプリット・バケット」 戦略のためのJSON。 0 がコントロールで 1 がテスト

また、 「room to grow (ゆとり) 」 戦略では、バケット間に十分な無効のスペースを残すことで、図 4 のように、既存の範囲のサイズ調整ができる。

room-to-grow

図 4 : 中間の無効部分に範囲の長さを更新し、コントロールとテストを拡大している

この 「ゆとり」 戦略を出来る限り使用することで、 JSON と Proctor Web アプリ両方に、より読みやすいテスト定義がもたらされるのだ。

便利なヘルパー・ユーティリティ

Proctor は Web アプリケーションのデプロイメントにおいて、 Proctor との作業をしやすくする、下記のようなユーティリティを提供している。

  • Spring controller下記の3種類のビューを提供;
    ① 現在のリクエストのグループ
    ② 現在のテスト・マトリックスの要約版
    ③    アプリケーションのテスト仕様で定義されているテストだけを含むJSONのテスト・マトリックス
  • Java servlet
    アプリケーションの仕様のビューを提供
  • テスト・バケット 内に自身を強制的に割り当てられる URL パラメータに対応(ブラウザの cookie 経由で永続)

私たちは、特別な権限を持った IP アドレスにのみ、本番環境でこれらのユーティリティにアクセスできるように許可しており、ユーザーの皆さんにも同様の設定を推奨したい。

あなたの会社でもきっと役立つ

Proctor は Indeed でのプロダクト開発における、データ・ドリブンなアプローチにとって必要不可欠なものとなった。

現在本番には、 100 件以上のテストと 300 件以上のテストのバリエーションが存在している。

Proctor のご利用を始めるには、クイックスタートガイドを是非ご覧いただきたい。

ソースコードを読み込んでみたい方、またはご自身の拡張を貢献してくださる方は、 GitHub  のページへどうぞ。