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

吉見和也(Kazuya Yoshimi) 2008-03-18 21:42:03

■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に移行しちゃいます。

押忍

※このエントリはZDNetブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet編集部の見解・意向を示すものではありません。

SpecialPR