前回までのあらすじ
RBSがテストになるとおもしろいんじゃないか日記1 - スペクトラム
RBSがテストになるとおもしろいんじゃないか日記2 - スペクトラム
RBSがテストになるとおもしろいんじゃないか日記3 - スペクトラム
間が開いた
結構間が開いてしまったがまだあきらめずに開発している。 サイドオーダーは全てのパレットをクリアして虹バッチとりました。
サイズ
プロパティベーステストの重要な概念にサイズがある。
サイズは簡単にいうと乱数に対する係数である。
Integerのサイズ0は0
、Arrayのサイズ0は[]
となる。
経験的にバグは0
や[]
の場合に起きることが多いことから、サイズは0から始めて、少しずつ大きくしていく手法をQuickCheckをはじめとした多くのプロパティベーステストツールでとっている。簡単に示すとこんな感じだ。
100.times do |size| float = Random.rand * size integer = float.to_i array = Array.new(integer) end
こうすることで単純なランダムよりも、不具合の起きやすいデータに集中してテストケースを作り出すことを目的としている。
このサイズを、これまで考えてきたものに実装するとどうなるか考えていく。
なんの引数?
これまで、Type.new("Integer").pick
というAPIを考えてきた。「Integer型から、1つ具体的な値をとりだすよ」という意味を表している。
サイズの概念を導入した時、最初はこう考えていた。
Type.new("Integer", size: 10).pick
コンストラクターにサイズを設定した方が、内部のコード的には簡潔になるからだ。 しかしながらこれは間違いだと気がついた。概念的におかしいからだ。「サイズ10のInteger型から1つ具体的な値をとりだすよ」と読めるが、「サイズ10のInteger型」とはなんだろう?サイズは増加させてテストケースを作る。「サイズ11のInteger型」を作るにはまた別のオブジェクトが必要になるだろうか?なんかしっくりこない。
変更してこうした。
Type.new("Integer").pick(size: 10)
違いはわずかだが、「Integer型からサイズ10で具体的な値を取ってくる。」と考えれる。サイズが増加しても「Integer型からサイズ11で値を取ってくる。」と考えれるのでオブジェクトは使いまわせる。サイズは取得時の引数と考える方がしっくりきそうだ。
どこからやってくるのか
サイズはどこからやってくるのだろうか。RaaPでは、1つのメソッドのオーバーロード毎にこの0〜100とサイズを変化させてテストを実行させることにした。
このオーバーロード毎に0~100のサイズを発生させればよさそうだ。この単位を MethodProperty
とした。
MethodProperty
MethodPropertyではこのサイズの変化を受け持つ他にも様々な役割を持たせた。ジェネレータからの値の具体化。メソッドの実行。返り値の型チェック。エラーハンドリング。タイムアウト。ループのスキップなどだ。
Result
ではMethodPropertyの実行結果について考える。 成功は考える余地がないほど単純だ。また、バグや型記述の不一致を見つけたいのだから当然失敗はほしい。
またなんらかの理由でどうしようもない場合がある。例えばNotImplementedError
が出た時は、型も何もないと考えられるので、スキップしたい。
他にもInteger.sqrt
の定義は(::int n) -> ::Integer
となっているが、実際はマイナスの数字を入力するとMath::DomainError
となる。
こういう場合はカスタムコードが必要となってくるが、現段階ではCLIだけの利用を考えているので、簡単に止まってしまっては困る。
というわけで、なんらかのエラーが出ても止まらず進めたい。この状態を例外としたい。
- スキップ: 型に関係なくどうしようもない場合。
- 例外: どうしようもなく例外が発生する場合。
TypeError
ではおもしろい考察として、TypeError
はどう扱うかという問題がある。
例外だと単純に考えていいだろうか?しかしながらTypeErrorはたいてい引数の型が間違っている時に発生する。Integerを期待している箇所にStringがきた場合などだ。これって型エラーなのでRaaP的には検知したい型エラー、つまり失敗のケースなんじゃないかと私は考えた。
よってTypeError
は例外の例外的に失敗に含めている。
bot
Kernel#raise
のような、絶対に返り値が発生しないメソッドも存在する。この場合はRBSではbot
を使用する。
逆にいうと、bot
が設定されている場合は例外が発生することを期待しているとも言える。
MethodPropertyでは、このbot
が返り値だったら例外が発生したら成功、しなければ失敗として扱っている。
再起型
RBSでは再起的な型を許可している。例えばリストにRBSを書いてみる。多分こんな感じ。
class List[T] attr_accessor head: T attr_accessor tail: List[T] def initialize: (head: T?, tail: List[T]?) -> void end
このclassList[T]
のインスタンスを生成したい時、tail
にList[T]
のインスタンスが欲しい。
ではこのtail
用のList[T]
のインスタンスを生成する時にもまたList[T]
のインスタンスが必要になってくる。
このような再起的な循環が簡単に起こり得るのでなんとかしたい。
今のところはアイデアはいくつかあるものの、実装に落とし込めてはいない。
ではどうするかというと、SystemStackError
が発生したら「どうしようもない場合」としてスキップすることにした。
シンボリックコール
これは最近実装した概念で、QuickCheckをはじめとしたプロパティベーステストで使われているデータ構造である。 レシーバー、メソッド名、引数のタプルでできていて、メソッドを呼び出す直前の状態をひとまとめにしている。 シンボリックコールはネストしてよく、コードのメソッド呼び出しそのものの構造を作ることができる。
シンボリックコールの嬉しいところは、メソッドの呼び出し状況の可視化が簡単になる点だ。 AをつくるにはBが必要で、BをつくるにはCが必要で……といったような複雑なオブジェクトの場合、テストが落ちた時のデータの再現が難しかったりする。シンボリックコールを残しておけば、用意に再現手順が作れるし、最悪シンボリックコール自体をdumpすればやりたいことがわかるようになっている。
基本的にデータはこのシンボリックコールで組み立てて、最後にeval
つまりはシンボリックコールを実行するようにした。