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

2008年03月10日 20:16
トラックバック (0)   コメント (0)
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 
      0 
    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)の結果を返すようにして完了

次で最後で押忍
最新エントリー
カテゴリ
月別のアーカイブ
プロフィール
吉見和也(Kazuya Yoshimi)
RAWHIDE.(ローハイド.)取締役兼最高技術責任者。Rubyで開発する心地よさに惚れ、Ruby道を邁進する日々。迷わず行けよ、行けばわかるさ、Ruby道。押忍!
Powered by
 

企画特集

ZDNet Japan ホスティング特集ZDNet Japan ホスティング特集
2008年夏のホスティングサービスのトレンドは何?
DELLが掲げる「新・仮想化アセスメントサービス」DELLが掲げる「新・仮想化アセスメントサービス」
〜企業システムの仮想化環境の構築を支援〜
Techno ExchangeTechno Exchange
仮想化技術がグリーンITにもたらすもの
ZDNet Japan Green ITZDNet Japan Green IT
サミットだけでは終わらせない!エンタープライズの取り組みはこれからだ!
フォトレポート:分解、アップル「iPhone 3G」
CNET News.comの姉妹サイトであるTechRepublicは、7月11日に発売されたばかりの「iPhone 3G」を早速分解し、その様子を紹介した。
ちょっと変わった「iPhone」向けアプリケーション10種
「iTunes」のApp Storeでは、「iPhone」向けのさまざまなアプリケーションが販売または無償で提供されているが、中にはちょっと変わったアプリケーションも存在する。
契約してわかった、iPhoneのさまざまな注意事項
7月11日にソフトバンクモバイルから発売された、アップル製携帯電話「iPhone 3G」。その契約手続きの中で、機種変更時の料金やメールの保存期間など、iPhoneが持つさまざまな注意事項が見えてきた。