@kyanny's blog

My thoughts, my life. Views/opinions are my own.

Mocha でスタブ/モックを使ったテストを書く

Mocha という Ruby のスタブ/モック用ライブラリを使ってテストを書いてみた。 Ruby もテストもスタブ/モックもよくわかってないので色々間違ってるかもしれないです。

スタブとモック

スタブとモックはちょっと違う物らしい。スタブは関数的というか、状態をもったり内部で複雑なことをしてくれるのを期待せず、決まりきった値を返して欲しいときに使うのかな、という風に理解してる。モックはもうちょっと高機能というか賢くて、メソッドが期待する引数とともに呼ばれたかどうかなど細かい条件つきでオブジェクトの振る舞いを偽装しテストできるようにする、のかなとか思ってるけどだいぶあいまい。

以下長いけど実際にテストコードを書いてみた。たぶんあんまり良い例じゃないです。

Ruby mocha test sample code · GitHub にも置いてあります。

スタブを使ったテスト

# -*- coding: utf-8 -*-
require 'test/unit'
require 'rubygems'
require 'mocha'

class Rubyist
  attr_accessor :boss

  def say
    "I love Ruby"
  end
end

class Boss
  attr_accessor :name
end

class TestRubyist < Test::Unit::TestCase
  def setup
    @rubyist = Rubyist.new
    boss = Boss.new
    boss.name = "Larry Wall"
    @rubyist.boss = boss
  end

  def test_love_perl? # Larry の前ではおべっかをつかう
    if @rubyist.boss.name == "Larry Wall"
      @rubyist.stubs(:say).returns("I love Perl")
      assert_equal @rubyist.say, "I love Perl"
    end
  end
end

モックを使ったテスト

# -*- coding: utf-8 -*-
require 'test/unit'
require 'rubygems'
require 'mocha'

class Rubyist
  attr_accessor :boss

  def say(message = "love Ruby")
    "I #{message}"
  end
end

class Boss
  attr_accessor :name
end

class TestRubyist < Test::Unit::TestCase
  def setup
    @rubyist = Rubyist.new
    boss = Boss.new
    boss.name = "Larry Wall"
    @rubyist.boss = boss
  end

  def test_love_perl? # Larry の前ではおべっかを使う
    if @rubyist.boss.name == "Larry Wall"
      @rubyist.expects(:say).with("love Perl").returns("I love Perl")
      assert_equal @rubyist.say("love Perl"), "I love Perl" # 引数が "love Perl" じゃなかったら fail
    end
  end
end

外部ライブラリに依存するコードのテスト

実際にはシンプルなスタブ/モックでどうにかなるものだけじゃなくて、もっと複雑なものをテストしたいことが多いと思う。以下のコードは XMLRPC::Client を内部で利用しているクラスのメソッドをテストしている、つもり。

request メソッドは XMLRPC::Client で通信した結果を利用して値を返すが、テストのときは XMLRPC::Client にいちいち通信させたくない(結果が一意とは限らないし)というときに、 XMLRPC::Client のインスタンスと似た振る舞いをもったオブジェクトを偽装して request メソッドの中で利用させている、つもり。

このへんになってくると、 Ruby のコーディング能力やら Mocha への理解度やらテストに対する知見やらがもろもろ不足しているせいで、テストコードが頻繁にエラーになってしまう。あちこちいじってどうにかこうにか実行できるテストを書けている、というていたらく。テストコードがエラーになるって本末転倒な感じで情けない。

そもそもこういう考え方で、テストの方針じたいがあってるのかどうかにも自信がない。ぐぐって見つかるのはどうしても使い方のお手本のようなものが多くて、実践的な例はそう豊富にはない。そういうのはやっぱりオープンソースソフトウェアのテストなどを探してたくさん読んで学んでいくしかないんだろうな。

# -*- coding: utf-8 -*-
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'xmlrpc/client'

module Sasimi
  class Blog
    def self.api
      'http://b.hatena.ne.jp/xmlrpc'
    end

    def self.request(method, url)
      client = XMLRPC::Client.new2(self.api)
      ok, result = client.call2(method, url)
      "やたー#{result}ブクマいったよー"
    end
  end
end

class TestSasimiBlog < Test::Unit::TestCase
  def test_request_get_total_count
    o = mock()
    o.expects(:call2).with('bookmark.getTotalCount', 'http://d.hatena.ne.jp/a666666/').returns([true, 100*100*100])
    XMLRPC::Client.expects(:new2).with('http://b.hatena.ne.jp/xmlrpc').returns(o)
    assert_equal Sasimi::Blog.request('bookmark.getTotalCount', 'http://d.hatena.ne.jp/a666666/'), "やたー1000000ブクマいったよー"
  end
end

# handle response content_type
module XMLRPC::ParseContentType
  def parse_content_type(str)
    a, *b = str.split(";")
    a = "text/xml" if a == "application/xml"
    return a.strip.downcase, *b
  end
end

#p Sasimi::Blog.request('bookmark.getTotalCount', 'http://d.hatena.ne.jp/a666666/')
#=> [true, 2826]