プライベートメソッドをパブリックインターフェースを使ってテストする方法

Jay Fields' Thoughts: Using Stubs to Capture Test Essence

たとえばこんなコードがある。

class MessageGateway
  def process(message_text)
    response = post(create_request(message_text))
    parse_response(response)
  end

  private

  def post(message)
    # ...
  end

  def create_request(message_text)
    # ...
  end

  def parse_response(response)
    # ...
  end
end

MessageGateway#processは3つのプライベートメソッドを呼んでいる…create_request, post, parse_response。

プライベートメソッドをテストするなら、__send__を使う方法がある。__send__はRuby 1.9でもprivate methodを呼ぶのに使える。

他にもMochaによるスタブとモックを使う方法もある。この方法は、スタブで偽装して、モックで引数をチェックできるから、結果的にプライベートメソッドのテストができるということだ。この方法ならばプライベートメソッド名を変更してもテストが壊れないという利点がある。
一見、何をテストしているのかわかりにくい気がするが、それは俺の修行不足なんだろうなぁ…なんせテスト対象のメソッド名が出ておらんし。プライベートだからメソッド名なんてどうでもいいってことか。
よくプライベートメソッドもテストすべきかで議論になっていたけど、スタブとモックによる方法でプライベートメソッドもテストできるから、いちおう議論に決着がついたのかな。

ついでにexpectationsによるテストも書いておいた。

class MessageGateway
  def process(message_text)
    response = post(create_request(message_text))
    parse_response(response)
  end

  def post(message)
    "<status>success</status>"
  end

  def create_request(message_text)
    "<text>#{message_text}</text>"
  end

  def parse_response(response)
    true
  end
end
 # !> method redefined; discarding old expects

###########################################################################
# Test::Unitによるテスト                                                  #
###########################################################################
require 'rubygems'
require 'mocha'
require 'test/unit'
class TestMessageGateWay < Test::Unit::TestCase
  # __with_mochaはスタブを使った振舞いベースなテスト
  # __with_sendは__send__を使った状態ベースなテスト
  def test_create_request__with_mocha
    gateway = MessageGateway.new
    gateway.expects(:post).with("<text>hello world</text>")
    gateway.stubs(:parse_response)
    gateway.process("hello world")
  end

  def test_create_request__with_send
    gateway = MessageGateway.new
    actual = gateway.__send__(:create_request, "hello world")
    assert_equal "<text>hello world</text>", actual
  end
  
  ################
  def test_post__with_mocha
    gateway = MessageGateway.new
    gateway.stubs(:create_request).returns("<text>hello world</text>")
    gateway.expects(:parse_response).with("<status>success</status>")
    gateway.process("")
  end

  def test_post__with_send
    gateway = MessageGateway.new
    actual = gateway.__send__(:post, "<text>hello world</text>")
    assert_equal "<status>success</status>", actual
  end

  ################
  def test_parse_response__with_mocha
    gateway = MessageGateway.new
    gateway.stubs(:create_request)
    gateway.stubs(:post).returns("<status>success</status>")
    assert_equal true, gateway.process("")
  end

  def test_parse_response__with_send
    gateway = MessageGateway.new
    actual = gateway.__send__(:parse_response, "<text>hello world</text>")
    assert_equal true, actual
  end
end

###########################################################################
# expectationsによるテスト                                                #
###########################################################################
require 'expectations'

Expectations do
  # test for create_request
  expect MessageGateway.new.to.receive(:post).with("<text>hello world</text>") do |gateway|
    gateway.stubs(:parse_response)
    gateway.process("hello world")
  end

  # test for post
  expect MessageGateway.new.to.receive(:parse_response).with("<status>success</status>") do |gateway|
    gateway.stubs(:create_request).returns("<text>hello world</text>")
    gateway.process("")
  end

  # test for parse_response
  expect true do
    gateway = MessageGateway.new
    gateway.stubs(:create_request)
    gateway.stubs(:post).returns("<status>success</status>")
    gateway.process("")
  end
end

# >> Expectations ...
# >> Finished in 0.00264 seconds
# >> 
# >> Success: 3 fulfilled
# >> Loaded suite -
# >> Started
# >> ......
# >> Finished in 0.003555672 seconds.
# >> 
# >> 6 tests, 6 assertions, 0 failures, 0 errors