本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはakiymこと秋山卓巳さんで、テーマは「Test2で変わるモダンなテスト」です。新たなテスティングフレームワークであるTest2を使ったテストの書き方や活用方法について紹介します。
本稿のサンプルコードは、本誌「WEB+DB PRESS Vol.106」のサポートサイトから入手できます。
Perlでのテスト
Perlではテストを書く際に、Test::More
というモジュールが広く使われています。これは、プログラムの実行結果が期待しているものと一致しているかをチェックするためのシンプルなモジュールで、テスト結果をTAP(Test Anything Protocol)形式で出力します[1]。
本稿のテーマであるTest2は、Test::More
などのバックエンドとして使われているTest::Builder
を置き換えるべく開発されているテスティングフレームワークです。2015年ごろよりChad Granum氏によって作られています。
Test::Builderの問題点
Test::Builder
は、Perlにおけるテストモジュールが行う基本的な動作を扱い、それをTAP形式で出力するモジュールです。
Test::Builder
の問題として、2001年ごろに作られたコードがメンテナンスされ使われ続けているため、拡張して使うのが難しいことがあります。テスト結果はTAP形式で出力されるため、1つのテストの結果が行単位で表され構造化されていないことから、テストモジュールのテストが困難になります。また、テスト出力のカスタマイズは、Test::Builder
の関数を直接上書きするモンキーパッチによる対応でしか行えない状況でした。
この問題を解決するため、拡張性に重点を置いた新たなテスティングフレームワークとしてTest2が開発されました。
ちなみに、Test::Builder
は今後どうなるのでしょうか。メンテナンスされないまま、古いコードが残っていくのでしょうか。実は、Test::Builder
の内部ではTest2のAPIを呼び出すように書きなおされており、すでに全面的な新規コードでの置き換えがされています。そのため、古いインタフェースを使っている既存のテストモジュールはそのままで、新たに書き始めるテストモジュールは柔軟なインタフェースを持つTest2を使えます。
Test2にすると変わること
Test2は、Test::Builder
やTest::More
が含まれるTest-Simple
ディストリビューションのバージョン1.3以降から使われるようになっています。また、Perl 5.26.0から標準モジュールとして同梱されるようになりました。
気になるのは、今動いている既存のテストをTest2にしたときにどうなるかでしょう。基本的には、Test::More
をはじめとするTest::Builder
を使った既存のテストモジュールが壊れることがないように、後方互換性を維持しつつ開発されています。ただし例外として、Test::Builder
をモンキーパッチによって拡張していた場合には正しく動作しないことがあります。また、いくつかのテストモジュールは、バージョンアップが必要になったり使えなくなったりします。詳しくは、Test2::Transion
を参照してください。
Test2::V0を使ったテスト
ここからは、Test2の拡張性をもとにして作られたテストモジュールTest2::V0
を使ったテストを紹介します。Test2::V0はTest2
の作者によって作られた、Test2-Suite
ディストリビューションに含まれるモジュールです。Test2はフレームワークであるため基本的な機能しか提供されていませんが、Test2::V0
では一歩踏み込んだ機能が実装されています。Test::More
で提供されるテスト関数が使えるのはもちろんのこと、複雑なデータ構造の比較やオブジェクトのモックといったテストにおけるさまざまな操作を行えます。
まずはcpanm
コマンドにより必要なモジュールをインストールします。Test2::V0
をインストールすると、Test-Simple
、Test2-Suite
ディストリビューションに含まれるモジュールも一緒にインストールされます。
なお、本稿ではPerl 5.26.2、Test-Simple 1.302136、Test2-Suite 0.000114を前提として説明します。
use Test2::V0
と書くことで、テストを行うための関数のエクスポートや、テストでの共通した処理の実行などをしてくれます。これはいわゆるボイラープレート(お決まりのテンプレート)の役割を果たします。
実際にビルトイン関数の動作を確かめる単純なテストを書いてみます。ここでは、Test2::V0
のo
k関数を使って、あるコードによる動作が正しい条件かどうかをテストします。
テストを実行すると次の結果が出力され、2つのテストが成功し、コードが意図したとおりに動作していることがわかります。
比較の基本
あるコードが正しく動作しているかをテストするために、得られた値が期待された値と一致しているかどうか比較したいことがあります。ここまでの例では、文字列比較の結果を真偽値としてok
関数に渡していました。is
関数を使うと、文字列や数値、配列、ハッシュといったデータ構造まで、比較演算子を指定することなく直感的に2つの値を比較できます。
もしコードが意図したとおりに動作していない場合、テストは失敗します。その場合に必要となるのは、どのテストがどのような状態で失敗したかという情報です。上記のテストをわざと失敗するように書き換えて実行します。
図1の実行結果を見ると、失敗したテストについて実際の値(GOT)と期待された値(CHECK)が出力されています。配列の何番目の要素が間違っていたのか、期待されたハッシュには存在しないキーがあるのかが、Test::More
に比べると読みやすい形式になっています。
ここまで、2つの値を比較する例を示しました。テストが複雑になってくると、値がundefではないことやハッシュのキーが存在するかどうかといったチェックをしたくなるかもしれません。Test2::V0
では、期待される値の条件を指定するしくみが用意されています。以降でそれらについて解説していきます。
値の比較
is
関数に渡した値はデフォルトで文字列による比較がされます(リファレンスを除く)。明示的に文字列、数値、真偽値による比較を行いたい場合には、string
、number
、bool
関数を使います(表1)。
表1 値の比較関数
関数名 | 説明 | 比較時の演算子 |
string | 文字列による比較 | $input eq $got |
number | 数値による比較 | $input == $got |
bool | 真偽値による比較 | ($input xor $got) ? 0 : 1 |
また、次に示すコードにあるように、数値を文字列として比較した場合や、!
演算子を使って否定した場合の真偽値の扱われ方については注意が必要です。必要に応じて、number
やbool
関数を使い分けるとよいでしょう。
正規表現と条件式による比較
正規表現にマッチする文字列であることをテストする場合は、matc
h関数を使います。また、条件式による比較を行いたい場合は、validator
関数を使います。コードリファレンスを渡し、比較時に与えられた値は$_で参照できます。
比較のショートカット
比較するデータ構造の要素が増えてくると、すべて列挙して比較するテストを書くのに時間がかかるため、もう少し省力的にテストをしたいことがあります。Test2::V0
には、要素に対して真偽値による比較や、ハッシュのキーが存在するかどうかだけを確認するといった、頻出する値の比較を行うためのショートカットが用意されています(表2)。
表2 値の比較ショートカット
関数名 | 説明 | データ例 |
T | 真偽値によるチェックの結果、真である | 1、'a' |
F | 真偽値によるチェックの結果、偽である | 0、'0'、''、undef |
D | 値がundefではない | 1、0、'0'、'' |
U | 値がundefである | undef |
DF | 値がdefinedであるが真偽値による評価では偽である | 0、'0'、'' |
E | 配列、ハッシュの要素が存在している | {a => 1}は{a => E}にマッチ |
DNE | 配列、ハッシュの要素が存在していない | {a => 1}は{b => DNE}にマッチ |
FDNE | 配列、ハッシュの要素が存在していない。またはその値が真偽値による評価では偽である | {a => 0}は{a => FDNE, b => FDNE}にマッチ |
データ構造の簡潔な比較
これまでは、配列やハッシュのリファレンスを渡すことでデータ構造の比較を行いました。データ構造が大きくなってくると見通しが悪くなり、さらには配列やハッシュ以外にもオブジェクトの比較がしたいということもあります。そこでTest2::V0
には、宣言的な比較をできるようにした、ビルダと呼ばれるデータ構造を比較する際の記述方法が用意されています。
ビルダによるデータ構造の比較
ビルダでは、コードブロック内に対象となる要素の列挙や条件を書けます。次のコードにあるように、配列の比較はarray
ビルダ、ハッシュの比較はhash
ビルダを使って行います。
(1)のitem
では、配列に含まれる要素を宣言した順に列挙しています。(3)のようにインデックスを指定することもできます。
(2)のend
はitem
で宣言された要素以外にないこと、(4)のetc
は宣言された要素以外にも存在し得ることを表します。これらを省略した場合、is
関数のときにはend
を呼び出したときと同じ挙動となりますが、わかりやすさのためどちらかを明示的に呼び出しておくことをお勧めします。
(5)では、ハッシュの比較をするためにfield
により対応するキーとバリューを列挙しています。
また、オブジェクトもobject
ビルダによって比較できます。
(1)では、bless
されているパッケージ名の比較を行います。
(2)では、a
メソッドの戻り値の比較を行います。(3)では、DNE
を渡し、b
メソッドが存在しないことをチェックしています。
ちなみに、直接リファレンスを渡すのとビルダを渡すのとではどちらが良いのでしょうか。前者は記述量が少ないというメリットがありますが、後者にはarra
yビルダであればすべての要素についてチェックを行うall_items
関数を使うことができ、順不同による比較をbag
ビルダで行うといった柔軟な対応ができます。また、リファレンスを扱う場合、前者では中身の要素それぞれが宣言された行番号を取得することはできません。後者ではitem
が関数の呼び出しとして実装されているため、テストの失敗時には対象となる行番号が詳細に出力されるといった良い点もあります。
isとlikeの違い
Test::More
ではlike
関数は正規表現による比較を行うものでしたが、Test2::V0
ではそれに加えデータ構造の比較も行えるようになっています。is
関数では厳格な(strict)比較、like
関数では緩やかな(relaxed)比較がされます。
この緩やかな比較とは、具体的にどのような動作をするのでしょうか。
まずはis
関数、like
関数での比較時のオプションについて知る必要があります。オプションには次の3種類あります。
- implicit_end
比較対象の配列、ハッシュについて、余分なインデックス、キーが存在しないことをチェックする
- use_regex
正規表現リテラルを直接渡すことを許可する。use_regex
が無効の場合は、match
関数を経由しなければならない
- use_code
コードリファレンスを直接渡すことを許可する。use_code
が無効の場合は、validator
関数を経由しなければならない
これらのオプションは、is
とlike
関数では表3のように対応されます。
表3 比較ルールと実装との対応
ルール(対応する関数) | implicit_end | use_regex | use_code |
strict(is) | ○ | × | × |
relaxed(like) | × | ○ | ○ |
like
関数を使い、正規表現やコードリファレンスを渡すテストの例を書いてみます。
(1)にあるハッシュの比較は、(2)のhash
とetc
を使った組み合わせと同様の動作をするため、このようなケースではlike
関数を使うことで簡潔に記述できます。
否定するテスト
テストを書いていると、ある条件を満たさないことといった否定した条件をチェックしたいときがあります。
このような場合のテストを表現するには、次のようなコードが考えられます。
1行目では、真偽値による比較で条件の判断を行っています。しかし、これではテストが失敗したときには$x
の値がテスト関数側に渡っていないため、出力結果に含めることができず、わかりにくいテストになります。
2行目では、is
関数の代わりにisnt
関数を使っています。テストが失敗したときには$x
の値が出力されますが、ネストしたデータ構造の一部分について条件を満たさない場合を表現できません。
そのため、is
関数とlike
関数では比較条件の否定が表現できるようになっています。比較を行う関数では否定演算子!
がオーバーロードされており、これを使うことで条件を否定するテストが簡単に書けるようになっています。
ここまで、値の比較について代表的な機能に絞って解説しました。ほかの比較機能について知りたい場合は、Test2::Tools::Compare
を参照してください。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT