RSpecチュートリアルやってみる あと2回

吉見和也(Kazuya Yoshimi) 2008-03-10 20:16:56

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編集部の見解・意向を示すものではありません。

SpecialPR

  • デジタル変革か?ゲームセットか?

    デジタルを駆使する破壊的なプレーヤーの出現、既存のビジネスモデルで競争力を持つプレイヤーはデジタル活用による変革が迫られている。これを読めばデジタル変革の全体像がわかる!

  • 「奉行シリーズ」の電話サポート革命!活用事例をご紹介

    「ナビダイヤル」の「トラフィックレポート」を利用したことで着信前のコール数や
    離脱数など、コールセンターのパフォーマンスをリアルタイムに把握するに成功。詳細はこちらから