2009-09-24

Mock と Stub について

はてなブックマーク   livedoor clip

初めまして、リコーの沖田です。この度私もこの blog を書くことになりました。以後よろしくお願いいたします。

みなさんテストは好きですか?私も含めて私の同僚は皆テストが大好きなので、しばしばテストの議論で白熱しすぎてしまいます。今日はそのテストの中から Mock(モック) と Stub(スタブ) について書いてみたいと思います。

Test Double

まずテストにおける Mock と Stub についてですが、これらは Test Double という概念の一部です。Double とは代役という意味で、テスト対象となるシステムが依存する外部のコンポーネントの代わりに、それらしく振舞ってくれるコンポーネントを代役として利用しようということです。

例えば Web アプリの Controller の単体テストがしたい場合に、Model の実装が完了するまでテストができないっていうのでは大変ですよね。そこで、Model の Mock や Stub を利用することにより、Model の影響を受けることなく、Controller の単体テストだけに専念することができます。

このように Test Double を利用することで、依存を排除し、テストを効率良く作成・実行することができます。

Test Double についての詳細は「xUnit Test Patterns(Gerard Meszaros, 2007)」という本や
http://xunitpatterns.com/Test%20Double.html
に書いてあります。なお、Mock と Stub 以外にも Spy, Fake, Dummy 等の Test Double もあるとのことです。

Mock と Stub の違い

さて、では Mock と Stub の違いはなんでしょうか。

先ほどの Test Double の定義では、Mock はテスト時の呼び出しの仕様をあらわしたもので、Stub はテスト時の呼び出しに対して決められた値を返すものです。

これを単に Mock は呼び出しの verification をして、Stub はそれをしないと認識してしまうと、どのようにして Mock と Stub を使い分けるかわからなくなってしまいます。さらにいえば、Mock と Stub が区別されている理由さえわからなくなります。

Mock と Stub の違いはテストの観点の違いです。相互作用(振る舞い)中心のテストに利用するのがMockで、状態中心のテストに利用するのがStubです。

相互作用中心のテストとはテスト対象のシステムと外部のコンポーネントとの間で正しいやり取りがされるかのテスト、いわばプロトコルのテストです。外部のコンポーネントは Mock により置き換えられ、システムから正しい呼び出しがなされているかを監視します。
したがって、例えばWebアプリの Controller の単体テストにおいて相互作用中心のテストが正しく行われてパスしているならば、Model が正しく実装された時に Controller が正しく動作するということが、Model が実装されなくても保障されます。

一方状態中心のテストとはテスト対象のシステムが正しい結果を返すかというテストです。したがって、最終的に返ってくる結果だけが重要なのですが、その過程において外部のシステムとの統合が面倒な時に Stub を利用してテストを簡単にします。なので、 Stub は信頼できる外部のコンポーネント(例えばDBとかLDAPとか)に対して適応するといいと思います。

相互作用中心のテストと状態中心のテスト

さて相互作用中心のテストと状態中心のテスト2種類の観点でのテストが出てきましたが、どちらのテストをやればよいのでしょうか? 結論からいうと両方やるべきです。

例えば BDD(Behavior Driven Development) では、その名前から振る舞いすなわち相互作用中心のテストだけをやればいいと錯覚してしまいます。たしかに Mock を利用して振る舞いの仕様を作成するのが BDD での spec 作成の大半を占めます。しかし、仕様書の観点からすると、振る舞いすなわちアルゴリズムだけの記述だけでは不十分で、入出力の例が書いてある必要があります。その入出力例を記述するのが状態中心のテストなので、両方やるべきなのです。

もちろん、コンポーネント毎に相互作用中心のテストと状態中心のテストのどちらに比重をおくべきかを考えることが重要になるわけですが。

RR(Double Ruby)

さて Mock を使った RSpec のテストを実際に書いてみようと思います。RSpec には標準で Mock が用意されているのですが、今回は RR(Double Ruby) というライブラリを利用してSinatraで作る blog のテストを書いてみようと思います。(記事の都合上基本的な Sinatra, RSpec, DataMapper の説明は省略しています)

まず RSpec の Mock を RR の Mock で置き換える設定をします。これで rspec の Mock が RRの Mock に置き換えられます。また、Sinatra のテストをするため、Rack::Test::Methods を include しています。

Spec::Runner.configure do |config|
  config.include Rack::Test::Methods
  config.mock_with :rr
end

次に、’/posts/1′ にアクセスすると、DataMapper により ID が 1 の記事が読み込まれ、その article が表示されるという仕様を書いてみます。

describe '/posts/:id' do
  it 'should load Posts with id' do
    mock(Post).get!('1') { mock(Post.new).article { 'article of 1' } }
    get('/posts/1')
    last_response.body.should == 'article of 1'
  end
end

Post は DataMapper で作成した Model です。その Post の Mock に対して get! が呼ばれることを verification しています。中括弧の中は article が呼ばれた時の返り値を指定しています。では、このテストが落ちることを確認したら実装をします。

def '/posts/:id' do
  post = Post.get!(params[:id])
  post.article
end

実装後テストをしてみると、実際にDBに値を入れなくても(DBの状態にかかわらず)テストが通ります。前述したように外部への依存がなく効率の良いテストができる ことが実感できます。

ちなみに RSpec の Mock を利用する場合はこのようにかけます。

Post.should_receive(:get!).with('1')\
  .and_return mock(:article => 'article of 1')

RR の方が実際のコードに近く、かつ短く書けるので素敵です。さらにいえば mock(Post.new) の部分を Machinist を利用して

Post.make_unsaved(:article => 'article of 1')

と書くとさらに素敵なのですが、Machinist については次回以降ということにします。

また、私の同僚が RR は RSpec に比べて matcher の機能が足りない部分があるので、既存の rspec での Mock を RR に置き換える時に困ることがあると言っていました。そこで RSpec と RR(0.10.2) の matcher を見比べたところ例えば hash_including という matcher で差異が見られました。

RSpec の matcher では hash_including は

Foo.should_receive(:color).with(:red, :blue => false)

のように Hash の key の存在確認と key value の組み合わせの両方が指定できます。
一方、RR では

mock(Foo).color(:red => true, :blue => false)

のように key value の組み合わせでしか指定できません。

これの解決には例えば satisfy という matcher を利用するか、RR::WildcardMatchers::HashIncluding.wildcard_match? を書き換えるかすればいいと思います。ここでは、satisfy を利用する例を紹介しておこうと思います。

def hash_has_keys(*args)
  satisfy do |hash|
    args.all? do |arg|
      arg.is_a?(Hash) ? arg.all? {|k, v| hash[k] == v } : hash.has_key?(arg)
    end
  end
end

これを以下のような感じで hash_including という matcherの代わりに利用すれば、

mock(Foo).color(hash_has_keys(:red, {:blue => false, :yellow => false}))

RSpec の hash_including と同様に使えます。

ただし、こうすると matcher が失敗した場合に、

unexpected method invocation:
  color({:red=>true, :green=>false, :blue=>false})
expected invocations:
- color(satisfy {block}) 

と表示されてエラーメッセージがいまいちわかりにくくなってしまうので注意してください。

長くなりましたので、最後にまとめますと、本エントリでは

  • Test Doubleの定義に基づく Mock と Stub の違い
  • Mock を利用する相互作用中心のテストと Stub を利用することができる状態中心のテストの使い分け
  • 相互作用中心のテストを RR の Mock を使って書く例および RR の tips

について説明しました。

2008-06-30

MogileFS を Ruby の mogilefs-client から触ってみた

はてなブックマーク   livedoor clip

日野原です。

今回はこの前に引き続いて MogileFS の話題です。

前回の最後に唐突に ruby から触ると言う話をした通り ruby のクライアントライブラリから MogileFS を操作してみましょう。

まず環境ですが、前回と同じ Fedora7 on coLinux です。
その上に、ruby と rubygems をインストールしておきます。

% sudo yum install ruby ruby-ri ruby-rdoc ruby-libs\
  ruby-irb ruby-devel rubygems

次に MogileFS のクライアントを gem でインストールします。

% sudo gem install mogilefs-client

ではこれで ruby のプログラムから触ってみましょう。
まずは下準備です。

% irb
irb(main):001:0> require 'rubygems'
=> true
irb(main):002:0> require 'mogilefs'
=> true

次に domain と class を作成します。(結果は見やすいように改行しています)

irb(main):003:0> mogadm = MogileFS::Admin.new :hosts => ["127.0.0.1:7001"]
=> #<MogileFS::Admin:0xb7649c10
              @readonly=false,
              @backend=#<MogileFS::Backend:0xb7649bc0
                                   @mutex=#<Mutex:0xb7649b70>,
                                   @lasterrstr=nil,
                                   @lasterr=nil,
                                   @hosts=["127.0.0.1:7001"],
                                   @socket=nil,
                                   @timeout=3,
                                   @dead={}>,
              @hosts=["127.0.0.1:7001"],
              @timeout=nil>
irb(main):004:0> mogadm.create_domain "hoge"
=> "hoge"
irb(main):005:0> mogadm.create_class "hoge", "important", 3
=> "important"
irb(main):006:0> mogadm.create_class "hoge", "normal", 2
=> "normal"

これでファイルを保存する準備が整いました。
ここで作った domain とは、保存するファイルのまとまりを表すもので、ファイルは必ず domain に属します。蓄積されているファイルを取得するときには、この domain と key の対を指定します。
key とはファイルごとに一意に定める値です。domain をまたがって key を一意にする必要があるかどうかはまだ調べていないので、興味のある方は自分で調べてみてください。
class はファイルの持つ属性で、class ごとにいくつのコピーを保持しておくかを指定することができます。ここでは、important というクラスを持つファイルは 3つのコピーを、normal クラスのファイルは 2つのコピーを保持しておくように指定しました。

では実際にファイルを保存してみましょう。
irb を起動したカレントディレクトリに test.jpg という JPEG 画像と、「Hello, world!」と書かれている ttt というテキストファイルを置いておきます。(結果は見やすいように改行しています)

irb(main):007:0> mog = MogileFS::MogileFS.new :hosts => ["127.0.0.1:7001"], :domain => "hoge"
=> #<MogileFS::MogileFS:0xb76328a8
                @readonly=false,
                @root=nil,
                @backend=#<MogileFS::Backend:0xb7632844
                              @mutex=#<Mutex:0xb76327f4>,
                              @lasterrstr=nil,
                              @lasterr=nil,
                              @hosts=["127.0.0.1:7001"],
                              @socket=nil,
                              @timeout=3,
                              @dead={}>,
                @hosts=["127.0.0.1:7001"],
                @domain="hoge",
                @timeout=nil>
irb(main):008:0> mog.store_file "testimage001", "normal", File.new("test.jpg")
=> 1441195
irb(main):009:0> mog.store_file "testtext001", "important", File.new("ttt")
=> 14
irb(main):010:0> mog.store_file "testtext002", "important", "ttt"
=> 14

store_file の引数は一つ目から順に key, class, ファイルです。
ファイルの指定は File オブジェクトでもファイル名でもいいらしいです。
また、store_content を使えば

irb(main):011:0> mog.store_content "testtext003", "normal", "I'm hungry..."
=> 13

のように文字列を直接保存することもできます。

ちなみに、これらのファイルは /etc/mogilefs/mogstored.conf の中で指定した docroot 以下に保存されています。
実際にちょっと見てみましょう。

% ls -l /var/mogdata/dev1/0/000/000/
合計 1420
-rw-r--r-- 1 root root 1441195 2008-06-26 21:23 0000000002.fid
-rw-r--r-- 1 root root      14 2008-06-26 21:24 0000000003.fid
-rw-r--r-- 1 root root      14 2008-06-26 21:25 0000000004.fid
% ls -l /var/mogdata/dev2/0/000/000/
合計 1424
-rw-r--r-- 1 root root 1441195 2008-06-26 21:23 0000000002.fid
-rw-r--r-- 1 root root      14 2008-06-26 21:24 0000000003.fid
-rw-r--r-- 1 root root      14 2008-06-26 21:25 0000000004.fid
-rw-r--r-- 1 root root      13 2008-06-26 21:28 0000000005.fid
% ls -l /var/mogdata/dev3/0/000/000/
合計 12
-rw-r--r-- 1 root root 14 2008-06-26 21:24 0000000003.fid
-rw-r--r-- 1 root root 14 2008-06-26 21:25 0000000004.fid
-rw-r--r-- 1 root root 13 2008-06-26 21:28 0000000005.fid
% file /var/mogdata/dev1/0/000/000/0000000002.fid
0000000002.fid: JPEG image data, EXIF standard
% cat /var/mogdata/dev1/0/000/000/0000000003.fid
Hello, world!
% cat /var/mogdata/dev3/0/000/000/0000000005.fid
I'm hungry...

ファイルサイズで判断すると、ちゃんとimportant で指定したファイルが 3つずつ、normal で指定したファイルが 2つずつ生成されていますね。
このファイル名は環境によっては異なってくるかもしれないので、バイナリファイルを cat してしまわないように気をつけてください。

ファイルを読み込むときは get_file_data に key を指定します。

irb(main):012:0> mog.get_file_data "testtext003"
=> "I'm hungry..."
irb(main):013:0> mog.get_file_data "testtext002"
=> "Hello, world!\n"

また、MogileFS::Admin を使ってデバイスや保存されているファイルの情報を取得することもできます。(結果は見やすいように改行しています)

irb(main):037:0> mogadm.get_stats
=> {"fids"=>{"max"=>5, "count"=>0},
    "device"=>[{"status"=>"alive", "files"=>"3",
                "id"=>"1", "host"=>"localhost"},
               {"status"=>"alive", "files"=>"4",
                "id"=>"2", "host"=>"localhost"},
               {"status"=>"alive", "files"=>"3",
                "id"=>"3", "host"=>"localhost"}],
    "replication"=>[{"files"=>"2", "class"=>"important",
                     "devcount"=>"3", "domain"=>"hoge"},
                    {"files"=>"2", "class"=>"normal",
                     "domain"=>"hoge", "devcount"=>"2"}],
    "file"=>[{"files"=>"2", "class"=>"important", "domain"=>"hoge"},
             {"files"=>"2", "class"=>"normal", "domain"=>"hoge"}]}

さて、これで最低限の読み書きはできました。
これだけできればきっと誰かが rails のファイルアップロードプラグイン(attachment_fuとか?)のバックエンドに MogileFS が使えるようにしてくれることでしょう。
楽しみですね。