RSpecチュートリアルやってみる 最終回

2008年03月18日 21:42
トラックバック (0)   コメント (0)
■A Game Is Afoot

ついにゲームとして遊ぶためのインタフェースに着手

クラス名はMastermind::Game
このクラスのコンストラクタは、第一引数にInteractorクラスのインスタンス、第二引数にRandomGeneratorクラスのオブジェクトを受け取り、Game#playメソッドにてゲームを開始する。

specs/game_spec.rb

require File.dirname(__FILE__) + '/../mastermind' 

describe "ゲームをする時" do
  before do
    @random = mock("random")
    @random.should_receive(:next).exactly(4).times.and_return 0, 0, 0, 0
    
    @interactor = mock("interactor", :null_object => true) 
    @game = Mastermind::Game.new(@interactor, @random) 
  end 

  it "秘密のコードを生成する" do
    @game.play
  end

  it "予想を入力するよう促す" do 
    @interactor.should_receive(:write).once.with("Enter your guess: ") 
    @game.play
  end
end

@interactorに代入しているmockオブジェクトの生成に:null_object => trueとある。これは定義されていないスタブメソッドが呼ばれた場合、mockオブジェクト自身を返すようにするオプション。つまり、一つ目のexampleで@interactorにwriteメソッドを作ってないけれど、エラーにはならないよということ。これを通すMastermind::Gameクラスは次の様になる。

src/game.rb

module Mastermind
  class Game
    def initialize(interactor, random_generator)
      @interactor = interactor
      @random_generator = random_generator
    end
    
    def play
      @code = Code.generate_using(@random_generator)
      @interactor.write("Enter your guess: ")
    end
  end
end

ここで、playメソッドでコードを初期化して、予想の入力を促しているけど、予想の入力は複数回あるんだから分けた方がいい。なので、入力部分をtake_turnメソッドとして分け、そこで入力を促すようにするようにspecを書き換える。

require File.dirname(__FILE__) + '/../mastermind' 

describe "ゲームをする時" do
  before do
    @random = mock("random", :null_object => true)
    @interactor = mock("interactor", :null_object => true) 
    @game = Mastermind::Game.new(@interactor, @random) 
  end 

  it "秘密のコードを生成する" do
    @random.should_receive(:next).exactly(4).times.and_return 0, 0, 0, 0
    @game.play
  end

  it "予想を入力するよう促す" do 
    @interactor.should_receive(:write).once.with("Enter your guess: ") 
    @game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white))
  end
end

Mastermind::Game#playはこんな感じになる。

def play
  @code = Code.generate_using(@random_generator)
  take_turn(1,@code)
end


次は実際にユーザーからの入力があった場合のexampleを書く。入力を受け付けるメソッドはInteractor#readline。それぞれの色の頭文字を4つ続けて入力してもらう。全部白ならwwww

it "ユーザーの予想入力を受け付ける" do 
  @interactor.should_receive(:readline).once.and_return 'wwww' 
  @game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white)) 
end

Mastermind::Game#take_turnでは、@interactor.readlineを一回呼んでやるようにする。

def take_turn(turn_number, secret_code) 
  @interactor.write("Enter your guess: ") 
  @guess_string = @interactor.readline
end

驚くべきは、ここまで@interactorに入るであろう、Interactorクラスを全く書いてない点。
ここまでテストが分離できると、モジュールごとの分担作業もやりやすそうだ。

そして予想があたったら、あんたの勝ちよって表示してあげないとね

it "一回目の予想で全部当たった" do
  @random.should_receive(:next).exactly(4).times.and_return 0 
  @interactor.should_receive(:readline).once.and_return 'bbbb' 
  @interactor.should_receive(:writeline).once.with('You won in 1 turn.') 
  @game.play 
end

単にテスト通すだけなら、playメソッドの中で、@interactor.writeline('You won in 1 turn.')を実行してあげればいいだけ。
でもこれじゃあ寂しいので、take_turnの中で入力をCodeオブジェクトに変換して、markメソッドで評価したい。ユーザーの入力をCodeオブジェクトにするのはCode#from_stringメソッド。

Mastermind::Game#take_turn

def take_turn(turn_number, secret_code) 
  @interactor.write("Enter your guess: ") 
  guess_string = @interactor.readline
  guess = Code.from_string(guess_string) 
  @score = secret_code.mark(guess)
end

Codeクラスには適当なCodeオブジェクトを返すfrom_stringメソッドを定義してあげたところで、ここまでのspecは全て通った。じゃあ、from_stringをちゃんと実装しましょうと、specから書く。

specs/code_creation_spec.rb

require File.dirname(__FILE__) + '/../mastermind' 

describe "文字列からコードを生成する時" do 
  it "wwwwが全部白のピンとい状態のCodeを返す" do 
    code = Mastermind::Code.new(:white, :white, :white, :white) 
    code.mark(Mastermind::Code.from_string('wwww')).should be_win
  end 
end

まずは単にMastermind::Code.new(:white, :white, :white, :white)が帰ってくるように、Mastermind::Code#from_stringを実装して、とりあえずテストを通す。その後、ちゃんと実装。結果こんなんなります

Mastermind::Code

@@colour_map = { 
  'b'=>:black, 
  'c'=>:cyan, 
  'g'=>:green, 
  'r'=>:red, 
  'y'=>:yellow, 
  'w'=>:white 
}

def self.from_string(code_string)
  pegs = code_string.split(//).collect {|each| @@colour_map[each]} 
  new(*pegs)
end

これでテストが通ってることを確認したら、全部白の時だけじゃなくて、いろんな色が混ざった場合のexampleもいくつか書いておきましょう。

■敗北を知りたい

specs/game_spec.rbに新しいexampleを追加する。予想があたった時に勝ちなのはいいが、何をもって負けとするのか。ここでは10回の予想が全部外れた場合に負けとする。

it "10回予想してもあたらなかったら負け" do 
  @random.should_receive(:next).exactly(4).times.and_return 0 
  @interactor.should_receive(:readline).any_number_of_times.and_return 'wwww'
  @interactor.should_receive(:writeline).once.with('You lose after 10 turns.') 
  @game.play
end

ここでもplayメソッドの中で@interactor.writeline('You lose after 10 turns.')を呼んであげればいいんだけど、ちゃんと実装したい気分。

Mastermind::Game

def play
  @code = Code.generate_using(@random_generator)
  turns = 0
  while turns < 10 && (not won?)
    turns += 1
    take_turn(turns, @code)
  end
  @interactor.writeline('You lose after 10 turns.') if turns == 10
  @interactor.writeline("You won in 1 turn.") if won?
end
    
def won? 
  (not @score.nil?) && @score.win? 
end

ふぅ、結構長いね、このチュートリアル。でもあと少し。毎ターンごとにヒットの数とブローの数を表示してあげないと成立しない。

specs/game_spec.rb

it "全部がヒット(black)の時の結果表示" do 
  @interactor.should_receive(:readline).once.and_return 'wwww' 
  @interactor.should_receive(:writeline).once.with 'Score: 4 black' 
  @game.take_turn(1, Mastermind::Code.new(:white, :white, :white, :white)) 
end
  
it "全部がブロー(white)の時の結果表示" do 
  @interactor.should_receive(:readline).once.and_return 'cgrb' 
  @interactor.should_receive(:writeline).once.with 'Score: 4 white'
  @game.take_turn(1, Mastermind::Code.new(:black, :cyan, :green, :red)) 
end
  
it "blackとwhiteがまじってる時の結果表示" do 
  @interactor.should_receive(:readline).once.and_return 'byrc' 
  @interactor.should_receive(:writeline).once.with 'Score: 1 black, 2 white' 
  @game.take_turn(1, Mastermind::Code.new(:black, :cyan, :green, :red)) 
end

これを全部通して、これまでmockを使ってきたinteractorを実装しておしまい。
記念すべき1回目は8ターンで正解。偶然に頼りすぎた

これで大体RSpecのこと覚えたかな。まぁ習うより慣れろで実戦投入です。rails2.0でTest::Unitがバグってるらしいですし、明日移行の新規プロジェクトではRAWHIDE.のテストは全部rspecに移行しちゃいます。

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

企画特集

ZDNet Japan ホスティング特集ZDNet Japan ホスティング特集
2008年夏のホスティングサービスのトレンドは何?
Webセキュリティ特集Webセキュリティ特集
Web2.0時代の脅威へ対抗するためのソリューションとは?
サーバ仮想化・グリーン化の利点を最大化!サーバ仮想化・グリーン化の利点を最大化!
そ多機能・高価値なNetAppストレージの秘密とは
APC SOLUTIONS FORUM 2008をレポートAPC SOLUTIONS FORUM 2008をレポート
電源、冷却の効率化によるエネルギー削減とは?
Techno ExchangeTechno Exchange
RackableとCTCの地球にやさしい関係
「シンプル」&「低コスト」な運用管理「シンプル」&「低コスト」な運用管理
IT運用管理に関するアンケート実施中!
セキュリティ対策レベルテスト公開!セキュリティ対策レベルテスト公開!
自社のセキュリティのウイークポイントはドコ?
ログ管理ソリューション特集ログ管理ソリューション特集
セキュリティ、コンプライアンス対策で注目度アップ!
ZDNet Japan Green ITZDNet Japan Green IT
サミットだけでは終わらせない!エンタープライズの取り組みはこれからだ!
フォトレポート:注目の「iPhone 3G」アプリトップ10
「iPhone 3G」発売から時間がたち、App Storeが充実してきた。ここでは、注目のアプリケーション10種を紹介する。
フォトレポート:コスプレーヤー、コミックの祭典に集合--Comic-Con 2008
Comic-Con 2008が7月にサンディエゴで開催された。このコミックの祭典とも言えるイベントで見つけたコスプレーヤーたちを画像で紹介する。
フォトレポート:絵で見る「Internet Explorer 8」ベータ2
マイクロソフトがこのほど、「Internet Explorer 8(IE8)」のベータ2をリリースした。このリリースにおける特長はユーザー志向の機能だ。