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を勉強してしまおうという、勢いだけのエントリでした。

蛇足というか、ここでは紹介しなかった機能等。

  • RushCheck使えば、思いもよらないバグを見つけることができる可能性がある。
  • RandomArrayの他に、RandomHashや、RandomProcもある。
  • もちろん、自分の作ったクラスもランダムに生成できる。
  • RushCheck::guardを使えば、ランダムに生成された値から、期待する値だけを使うこともできる。
  • ランダムに生成された値で、特定の値が何%あったかという情報は、trivialや、classifyで取得することができる。
  • RuchCheck::checkが、結果をBoolで返すので、みんな大好きRSpecと組み合わせてもおk。

続きは、こちらでTutorial of RushCheck - RushCheck