北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

perlのテストの仕組み(TAP::Harness)

perlのテストフレームワークである、TAP::Harnessの動きを調べました。わかりやすく言えばproveコマンドの中身です。

*.t(予備知識)

説明不要だと思いますが、モジュール開発者が書くテストスクリプトです。個々の*.tは単なるスクリプトで、実行されるとTAPを標準出力へ吐きます。こんなの。

1..2
ok 1
not ok 2

TAPを吐くのは、Test.pm とか Test::Moreとかが担当しているお仕事で、TAP::Harnessは何もしないです。

なお、書式さえ守ってればTest.pmやTest::Moreを使わなくても単にprint文で吐いてもOKだったりします。

App::Prove

proveコマンドの実装です。引数の解釈とかをして、TAP::Harnessを起動します。

TAP::Harness

テストを起動させるruntestsを持っています。runtestsに*.tのファイル名をどさっと渡すと、片っ端から起動してテスト結果を集めて表示してくれます。直列での実行だけでなく、並列実行とかもできます。

TAP::Harnessは、後述するParserとFormatter::Session(とFormatter)とAggregatorをコントロールしてテストを進めます。大まかな流れとしては、

  1. テストスクリプト1つに対して、1つのParserとFormatter::Sessionを作る
  2. ParserがTAPを解析してトークン(Parser::Result)を作る
  3. Formatter::Sessionがトークンを整形して途中経過の表示を行う
  4. Aggregatorに結果を登録する
  5. テストスクリプトの個数だけ、1〜4を繰り返す
  6. FormatterがAggregatorを解析し、サマリを表示する

となります。

なお、runtestsの他にaggregate_testsと言うメソッドも持っているのですが、複数種類のHarnessによるテスト結果を一つのAggregatorでサマリしたいとき以外は使う必要はないと思います。

Parser

テストを起動してTAPを吐かせて、それを解析してトークンに分けます。

二つのイテレータ

Parserでは「TAPの読み込み」と「トークンの切り出し」の双方をイテレータとして扱うので、混乱しないように注意が必要です。

前者のTAPの読み込みは TAP::Parser::Source の get_streamの戻り値である TAP::Parser::Iterator が実体となります。*1

後者のトークンに関しては、Parser自体がイテレータとなっていて $parser->next のように呼び出します。TAP::Parser::Resultオブジェクトが順番に返ってきます。

テストの起動

個々のテスト(*.t)は、Parserが作成されるときに、

  1. コンストラクタからTAP::Parser::Source::Perlのget_streamが呼ばれる*2
  2. TAP::Parser::IteratorFactoryのmake_iteratorが呼ばれる*3
  3. TAP::Parser::Iterator::Processのnew で起動

みたいな感じで起動されます。

TAPの解析

TAP::Parser::Grammar がstream(TAP::Parser::Iterator)からの入力を解析します。tokenize でTAPの各行を TAP::Parser::Resultオブジェクトに変換して返します。

テスト結果の集計

個々の*.tのテスト結果は、Parserが保持します。(トークンを戻す)イテレーションごとに、結果が蓄積されます。

パースが完了した後は、tests_runとかhas_problemsとかpassed、failed等のメソッドで結果を取り出せます。

FormatterとFormatter::Session

ParserとAggregatorからの出力を加工して表示します。デフォルトではConsole用とFile用の二つのFormatterが用意されてますが、どちらも文字列としての出力にしか対応してないので、Perlのデータ構造でテスト結果を得たい時はこれらを自作するとよさげです。

FormatterとFormatter::Session は組になっていて、以下のような仕事の分担をします。

  • Formatter::Session->result → Parserからのトークンの詳細を表示(Parser::Result)。*4
  • Formatter->summary → 全テストの実行結果をサマリ表示(Aggregator)。

なお、Formatter::Sessionのインスタンス化する義務はFormatter->open_testにあるみたいです。個々の*.tに対してopen_testが呼ばれるので、適切なFormatter::Sessionを作り、返す必要があります。Formatter->open_test に対する close_testはFormatter::Session側に実装します。

Aggregator

テスト結果(Parserオブジェクト)をHarnessから詰め込まれます。Parserがある程度サマってくれてるので、それを合計するのが主な仕事となります。

Harness->runtestsの戻り値でもありますが、この中にはテスト結果の詳細(Parser::Result)は入ってません。

まとめ

Parserがテストの起動と集計に片足(両足?)を突っ込んでいて、ここに責務が集中しています。

また、テスト結果はFormatterとFormatter::Sessionに流れ込んでそのまま標準出力行きとなってます。Perlのデータ構造でテスト結果を得たい時は、ここでキャッチするといいです。

*1:IteratorFactoryを経由します。

*2:*.t 以外(rubyとかphpとか)でもTAPさえ吐けばTAP::Parser::Sourceになれる

*3:配列、ストリーム、プロセスなど、どの種類の(TAPを返す)イテレータを作るか選んでいる

*4:要は、テスト時のログを全行受け取って表示できるってこと。