rushcheck事始め
人や自分の書いたコードを書き直して、簡単なテストをすました後に、ふとホントにこれで大丈夫なのか、不安になるときがある。そんな不安を解消するのにRubyであれば、ikegamiさんが作られたRushCheckがディモールとよいので、ためしてみる。
RubyのArrayは、配列の先頭は簡単にとりだせるのに、残りは割と面倒なので、こんな関数を書いてみる。
def cdr(ary) ary - [ary[0]] end
関数はかけた。後は、これが思ったように動くか試すだけ。なので、こんなテストコードを書いてみる。簡単なテストにしたいので、TestUnitは使わない。
ary = (1..10).to_a p( ary == [ary[0],cdr(ary)].flatten) $ ruby cdr.rb true
ちゃんと、後ろが取り出せているようだ。
ただ、冷静になってみると、最初に用意したデータが都合がよかっただけではないか、無意識で大丈夫なデータを用意していないかと考えてしまい、不安になる。そんなときに、RushCheckが便利。
RushCheckを試すには、RushCheck::Assertionのインスタンスを生成し、checkを実行するだけの簡単なお仕事で、ランダムなテストをデフォルトで100回実行してくれる。
こんな感じである。
$ cat succ.rb require 'rubygems' require 'rushcheck' def nexti(i) i + 1 end rc = RushCheck::Assertion.new(Integer) do | i | i.succ == nexti(i) end rc.check $ ruby succ.rb OK, passed 100 tests.
RushCheck::Assertion.newは、引数に生成したいクラスをとり、ブロック引数として、ランダムに生成したインスタンスを渡す。ブロックは、Boolの値を返すようにする。上のsucc.rbでは、引数として"Integer"を渡したが、デフォルトでは他に、"Float","String"などが扱える。
テストが失敗したときは以下のようになる。
$ cat fail_succ.rb require 'rubygems' require 'rushcheck' def nexti(i) i + 2 end rc = RushCheck::Assertion.new(Integer) do | i | i.succ == nexti(i) end rc.check $ ruby fail_succ.rb Falsifiable, after 1 tests: [0]
これは、1回目のテストでfailになり、そのときの値は0であることを示している。
もし、ランダムな値が複数欲しいときは、どうしたらいいのだろうか?
答えは簡単である。引数に欲しいだけ渡してやればよい。
RushCheck::Assertion.new(Integer,Integer,....,Integer) do | i1 , i2 ,..., ix | exp end
では、配列が欲しい場合は、どうしたらよいのだろうか?
引数に沢山渡してやって、ブロックの中で配列にするのだろうか?
答えは、半分正解、半分否。
たしかに、そのやり方でもできるが、配列の長さが固定になってしまう(それでも、良い場合もあるんだけど)。何よりも、沢山の要素をもつ配列を作りたいときは、激しくめんどくさい。
では、どうするかというと、RandomArrayのサブクラスを作り、それを引数として渡してあげる方法がとれる。
class MRArray < RandomArray;end MRArray.set_pattern(Integer){|a,i| Integer}
RandomArrayのクラスメソッドのset_patternを使い、どのような要素をもつ配列かを定義する。引数は、基本となるクラス。ブロック引数には、そのとき生成される配列全体とインデックスが渡される。それらを使い、自分の欲しい配列を作ることができる(Tutorialでは、インデックスが偶数の要素はInteger,奇数ならばStringという配列を作ったりしている)。
というわけで、最初に書いたcdrメソッドのテストは、こんな感じになる。
$ cat cdr.rb require 'rubygems' require 'rushcheck' def cdr(ary) ary - [ary[0]] end ary = (1..10).to_a p( ary == [ary[0],cdr(ary)].flatten) class MRArray < RandomArray;end MRArray.set_pattern(Integer){|a,i| Integer} RushCheck::Assertion.new(MRArray) do | ary | ary == [ary[0],cdr(ary)].flatten end.check $ ruby cdr.rb true Falsifiable, after 2 tests: [[-1, -1]]
Arrayの(-)は、差集合を作るので、a1 - a2 とした場合、a2に入っている要素がa1に複数存在するとだめになってしまうわけだ。最初に要した配列では、配列作るのが面倒だったので、Rangeから配列を作ったため、このバグが見つからなかったわけだ。
最後に、ここまでのまとめとして、勝手に添削をさらに勝手に書き直すで、書き直したmake_clusterメソッドが、もともとの実装と同じ動作をするかテストしてみる。
$ cat make_cluster.rb require 'rubygems' require 'rushcheck' def make_cluster(centroid, num_array) c = num_array.select{|v| (centroid[0] - v)**2 < (centroid[1] - v)**2} [c,(num_array - c)] end def before_implementation(centroid, num_array) cluster = [[], []] num_array.each {|val| if (centroid[0] - val)**2 < (centroid[1] - val)**2 cluster[0] else cluster[1] end << val } cluster end class MRArray < RandomArray;end MRArray.set_pattern(Float){|a,i| Float} RushCheck::Assertion.new(Float,Float,MRArray) do | f1 , f2 , ary | centroid = [ f1 , f2 ] make_cluster(centroid, ary) == before_implementation(centroid, ary) end.check $ ruby make_cluster.rb OK, passed 100 tests.
まぁ、正直言いますと、偉そうに書き直してはみたんだけど、ホントにいいんだろうかと不安になってしまった(cdrでもやっているが、差集合を使っての不具合がでないか。でも、元の実装から考えて、引く側、ひかれる側で重複する要素がでないのはわかっているんだけど)ので、この不安を解消するついでに、RushCheckを勉強してしまおうという、勢いだけのエントリでした。
蛇足というか、ここでは紹介しなかった機能等。