「Rubyベストプラクティス」を読む1


数か月前に、一度「Rubyベストプラクティス」を読んでいたけれど、良い内容だったので
復習のために、もう一度読みなおしてみる。大事そうなところを日記に書いておこう。

まずは1章から、テストの話。

メソッドを分割する

本に載っている例。以下のようなコードをテストするとする。

class Questioner
  # questionの文字列を出力して、標準入力を待つ。
  # 入力の内容によって、以下のような動きをする
  # y Y Yes YeS YES yes など => trueを返す。
  # n N No nO など => falseを返す。
  # それ以外 => もう一度 askを呼ぶ。
  def ask(question)
    puts question
    response = gets.chomp
    case(response)
    when /^y(es)?$/i
      true
    when /^no?$/i
      false
    else
      puts "I don't understand."
      ask question
    end
  end
end

まずはテストしやすいように、コードを分割する。この場合、getsをほかの部分と分離させる。

class Questioner
  def ask(question)
    puts question
    response = yes_or_no(gets.chomp)
    response.nil? ? ask(question) : response
  end

  def yes_or_no(response)
    case(response)
    when /^y(es)?$/i
      true
    when /^no?$/i
      false
    end
  end
end

これで、yes_or_noメソッドは簡単にテストできる。例えば、minitestを使えば

def setup
  @questioner = Questioner.new
end

def test_yes
  %w(y Y Yes YES yes).each do |yes|
    assert @questioner.yes_or_no(yes), "#{yes.inspect} expected to parse as true"
  end
end

とできる。askメソッド以外はテスト出来るようになる。

スタブを使う

次に、askメソッドを呼び出しているようなメソッドをテストする場合。

class Questioner
  def inquire_about_happiness
    ask("Are you happy?") ? "Good" : "That's Too Bad"
  end
end

askは内部でgetsを使っているので、テストしにくい。このようなときは、特異メソッド定義を使って、askをスタブで置き換える。

def setup
  @questioner = Questioner.new
end

def test_yes
  def @questioner.ask(question); true; end
  assert_equal "Good", @questioner.inquire_about_happiness
end

def test_no
  def @questioner.ask(question); false; end
  assert_equal "That's Too Bad", @questioner.inquire_about_happiness
end

モックを使う

putsやgetsを使っているaskメソッドをテストするには、IOオブジェクトのように振舞うオブジェクトを使えばいい。
まずはputsやgetsを、IOオブジェクトへのメソッド呼び出しに書き直す。

class Questioner
  def initialize(in=STDIN, out=STDOUT)
    @input, @output = in, out
  end

  def ask(question)
    @output.puts question
    response = yes_or_no(@input.gets.chomp)
    response.nil? ? ask(question) : response
  end
end

テストではIOオブジェクトをStringIOなどにすれば、askメソッドもテストできる。

def setup
  @in = StringIO.new
  @out = StringIO.new
  @questioner = Questioner.new(@in, @out)
  @question = "Are you happy?"
end

def test_ask
  @in << "yes"
  @in.rewind
  assert @questioner.ask(@question), "Expected yes to by true";
  assert_equal "#{@question}\n", @output.string
end