MastermindをRSpecでTDDやっちゃいますの続きですよー
ルールはこちら(Wikipedia)
前回は、出題者が全て白のピンをたてたけど、解答者が全て黒と予想した時のspecを書いたところまで。今回はそのテストを通すところから。
この場合、
1 ヒットの数を返すメソッドMastermind::Score#blackが0
2 ブローの数を返すメソッドMastermind::Score#whiteが0
3 解答者の勝利ではない
となるはずで、specにもそう書いてある。現在通ってないテストは1と3
予想を評価してるのはMastermind::Code#mark。勝ちを判定しているのがMastermind::Score#win?。なのでそれぞれ書き換える
Mastermind::Code#mark attr_accessor :pegs def mark(guess) black = (0..3).inject(0) do |b,n| @pegs[n] == guess.pegs[n] ? b + 1 : b end Score.new(black,0) end Mastermind::Score#win? def win? self.black == 4 end
これでテストが通るようになりました。
そして次
赤白白白に対して黒黒黒赤と予想した時、
1 ヒットは0
2 ブローは1
3 解答者の勝利ではない
となるけど、チュートリアルには2番のexampleしか書かれてない。テスト駆動開発のフローの一番最初に、テストを書いてそれが失敗することを必ず確認するとあり、1と3はすでにそうなってるからexampleを書く必要はないよということか。でも心配だし書いちゃいたいなぁ。でも書かないことのメリットがあるのかもしれないから、素直に通らない2番のexampleだけ書いて次に進む。
require File.dirname(__FILE__) + '/../mastermind' describe "ピンと予想で、1つのピンが色が正しいけど場所が違う時" do before do code = Mastermind::Code.new(:red, :white, :white, :white) guess = Mastermind::Code.new(:black, :black, :black, :red) @score = code.mark(guess) end it "ブローの数(Score#white)が1" do @score.white.should == 1 end end
で、これが通るようにCode#markを修正
def mark(guess) black = (0..3).inject(0) do |b,n| @pegs[n] == guess.pegs[n] ? b + 1 : b end white = (0..3).inject(0) do |w,n| @pegs.include?(guess.pegs[n]) ? w + 1 : w end white -= black Score.new(black,white) end
チュートリアルとはなんか違ってるけど気にしない。テストは通ってるんだから。
チュートリアルではこの後リファクタリングをして、もういくつか他のケースについても境界値テストをする必要があると言ってる。まぁ、必要になったらその時書きますよ、と思って流し読み。
■Randomness and Mocks
18ページから新展開。
出題者側のピンをランダムに生成したい。
まずはそのspecファイルsecret_code_generation_spec.rb
ここでは乱数の発生にmockを使っている。random_generatorを先に作らず、わざわざmockでやってるのは、ランダムな値を使っていたら、境界値テストにならないから。
最初のexampleだけでは、mockの書き方がよくわからなかったので、その次のステップも一緒に進めました。
require File.dirname(__FILE__) + '/../mastermind' #0=Black, 1=Cyan, 2=Green, 3=Red, 4=Yellow, 5=White describe "コードジェネレーターは" do before do @random = mock("random") end it "ランダムシーケンスが全て0を返す時、全て黒のピンを立てる" do @random.should_receive(:next).exactly(4).times.with(no_args).and_return 0,0,0,0 code = Mastermind::Code.generate_using(@random) code.mark(Mastermind::Code.new(:black, :black, :black, :black)).should be_win end it "ランダムシーケンスが0,1,2,3と返してくる時、Black Cyan Green Redの順でピンを立てる" do @random.should_receive(:next).exactly(4).times.with(no_args).and_return 0, 1, 2, 3 code = Mastermind::Code.generate_using(@random) code.mark(Mastermind::Code.new(:black, :cyan, :green, :red)).should be_win end end
@random = mock("random")でmockオブジェクトを作成。
@random.should_receive(:next)はスタブメソッドを設定。簡単に言うと@randomにnextというメソッドを定義。
その後のexactly(n).timesは、このスタブメソッドが指定回数以外の呼び出しがあったらエラーになる。というふうにドキュメントにはあるのだけど、ここの指定を変えてみてもエラーになったりならなかったり。要調査
でもってwith(no_args)で呼び出し時に引数がないことを指定。
最後にand_returnで実行された時に返す値を,区切りで列挙
and_return 0,1,2,3ってやると@random.nextを呼び出した時に1回目は0、2回目は1の様に返してくれる。
で、Mastermind::Code.generate_usingがundefinedなのでつくる。
@@colours = [:black, :cyan, :green, :red, :yellow, :white] def self.generate_using(random_generator) pegs = [] 4.times {pegs << @@colours[random_generator.next]} new(*pegs) end
これで乱数ジェネレータから帰ってきた数字に基づいて、コードを生成することができるようになりました。
■RandomGenerator
今度は今までmockで代替してきたrandom_generatorをつくります。
最初にspecの内容
require File.dirname(__FILE__) + '/../mastermind' describe "乱数ジェネレータは" do before do @random_generator = Mastermind::RandomGenerator.new end it "0から5の範囲外の数値を生成しない" do 1000.times do number = @random_generator.next number.should be_kind_of(Numeric) number.should satisfy {|n| n >= 0} number.should satisfy {|n| n < 6} end end end
サンプル数を1000個として、それぞれが0-5の範囲にあるよという仕様らしい。1001回目に6が出たら通ってしまうけど気にするだけ無駄だよね。
satisfyというmatcherは、後ろに続くブロックの中身を評価するものらしい。
number.should >= 0
number.should < 6
とも書けるけど、チュートリアルだしいろんな書き方があることを教えてくれてるのね。ありがたい
で、これを通す実装
module Mastermind class RandomGenerator def next end end end
常に0を返して、とにかく通してからexampleを追加する
it "0-5の数値を平均的な分布で返す" do buckets = [0, 0, 0, 0, 0, 0] 10000.times {buckets[@random_generator.next] += 1} (0..5).each do |each| buckets[each].should satisfy {|n| n > 1500} buckets[each].should satisfy {|n| n < 1800} end end
今度は1万回やって、それぞれの数値の出現回数が1500回から1800回ならよしとするということか。均等な分布なら1666回程度の出現回数なので誤差150回は認めるということね。
ここらへんの記述って、プログラムの精度をどこまで求めるのかってことまで表現できるのですね。なかなか面白い
Mastermind::RandomGenerator#nextをrand(6)の結果を返すようにして完了
次で最後で押忍
※このエントリはZDNETブロガーにより投稿されたものです。朝日インタラクティブ および ZDNET編集部の見解・意向を示すものではありません。