Chapter 14 Spec::Mocks
あんまり需要なさそうなのでメインブログから引っ越し。mockについて。
14.1 Test Doubles
Test Doubleは他のオブジェクトの代わりをするオブジェクト
Spec::Mocks
- double("double")
- stub("stub")
- mock("mock")
これらのメソッドは全部同じで Spec::Mocks::Mock クラスのインスタンスを作成する。引数の文字列は option 扱いだけれど、 failure message の中で使われるので入れておいた方がいい。
14.2 Method Stubs
Method Stub はあらかじめ決めておいたresponseを返すメソッドのこと。
describe Statement do it "uses the customer's name in the header" do customer = double('customer') customer.stub(:name).and_return('Aslak' ) statement = Statement.new(customer) statement.generate.should =~ /^Statement for Aslak/ end end
上記のテストをパスさせる最低限のコードは下記のようになる。
class Statement def initialize(customer) @customer = customer end def generate "Statement for #{@customer.name}" end end
14.3 Message Expectations
message expectation は指定したメソッドが呼ばれなければfailureになるstub。下記のようにstubの代わりにshould_receiveを使う。
describe Statement do it "uses the customer's name in the header" do customer = double('customer') customer.should_receive(:name).and_return('Aslak') statement = Statement.new(customer) statement.generate.should =~ /^Statement for Aslak/ end end
Geven, Then, When?
describe Statement do it "logs a message on generate()" do customer = stub('customer') customer.stub(:name).and_return('Aslak' ) logger = mock('logger') statement = Statement.new(customer, logger) logger.should_receive(:log).with(/Statement generated for Aslak/) statement.generate end end
上記の書き方だと
logger.should_receive は event(statement.generate) の前に来ないといけないのが、これまでの習慣(前提条件→実行→検証)と異なってしまってちょっと変な感じ。RSpecではSpyは提供してない。RRとnot-a-mockのようなライブラリを使うと、event のあとにshouldが使える
not-a-mockだとこう書ける
describe Statement do it "logs a message when on generate()" do customer = stub('customer') customer.stub(:name) logger = mock('logger') logger.stub(:log) statement = Statement.new(customer, logger) statement.generate logger.should have_received(:log) end end
RRだとこう書ける
describe Statement do it "logs a message when on generate()" do customer = Object.new stub(customer).name logger = Object.new stub(logger).log statement = Statement.new(customer, logger) statement.generate logger.should have_received.log end end
Spyとは
スパイ(Spy)
Ruby Freaks Lounge:第35回 実用的なダミーサーバ ww(double-web)(1)|gihyo.jp … 技術評論社
自動化テストから呼び出されたことを事後に検証するための情報をとっておく手法です。簡単な応答をしたあとに,実際の呼ばれ方も検証したい場合に使います。
14.4 Test-Specific Extensions
実在のオブジェクトにmock的な拡張を施す話。
describe WidgetsController do describe "PUT update with valid attributes" it "redirects to the list of widgets" do widget = Widget.new() Widget.stub!(:find).and_return(widget) widget.stub!(:update_attributes).and_return(true) put :update, :id => 37 response.should redirect_to(widgets_path) end end end
stub! メソッドで既存のオブジェクトやクラスを拡張できる。
Partial Mocking
mock(message expectations)毎にexampleを分けることをpartial mockingと呼んでるっぽい。そうすることでcontroller用のexampleをちょうどよい粒度で分割できたりする。
mockやstubは少なめにするよう気をつけた方がいい。多くするとうまくいかなくなる。
One Line Shortcut
ある値を返すメソッドを定義したstubオブジェクトを作る一番簡単なメソッドは下記
customer = double('customer', :name => 'Bryan')
double, mock, stubメソッドは第二引数にhashをとって、メソッド名と戻り値の設定にする。上記の書き方は下記の書き方と同じ。
customer = double('customer') customer.stub(:name).and_return('Bryan' )
hashのkey/valueペアは好きなだけとれる。
Implementation injection
引数によって戻り値を変えるstubを用意するには、下記のようにブロックを使う。
ages = double('ages') ages.stub(:age_for) do |what| if what == 'drinking' 21 elsif what == 'voting' 18 end end
beforeでstubを定義して、複数のexampleで使うようなときに有効。でもデータと計算方法をexampleから分割してしまうので、shouldとかで比較されない単純な数値や文字列などを返す場合に使うべき。
Stub Chain
下記のようなメソッドチェーンのdoubleを作るとする。
Article.recent.published.authored_by(params[:author_id])
普通に書くとこうなる
recent = double() published = double() authored_by = double() article = double() Article.stub(:recent).and_return(recent) recent.stub(:published).and_return(published) published.stub(:authored_by).and_return(article)
めんどい。こんな時は stub_chain メソッドを使うと楽に書ける。
article = double() Article.stub_chain(:recent, :published, :authored_by).and_return(article)
14.6 More on Message Expectations
Counts
shoud_receiveの回数制限をする手法が用意されている。
# 一回だけwithdrawメソッドが呼び出されるのを期待する mock_account.should_receive(:withdraw).exactly(1).times # 五回以内open_connectionが呼ばれるのを期待する network_double.should_receive(:open_connection).at_most(5).times # 二回以上open_connectionが呼ばれるのを期待する network_double.should_receive(:open_connection).at_least(2).times
exactly(1).times と exactly(2).timesにはショートハンドが定義されている。
account_double.should_receive(:withdraw).once account_double.should_receive(:deposit).twice
Neagative Expectation
あるメソッドが呼ばれないことを期待する書き方もいくつか用意されている。
# 一番標準的な書き方 network_double.should_not_receive(:open_connection) # 上のと同じ network_double.should_receive(:open_connection).never # 同じ network_double.should_receive(:open_connection).exactly(0).times
Specifying Expected Arguments
メソッドを使ったかどうかだけではなくて、引数もチェックしたいときには with メソッドを使う。引数が複数あるときには、withメソッドに複数の値を渡す。==メソッドで値が等しいかを判定している。
account_double.should_receive(:withdraw).with(50) checking_account.should_receive(:transfer).with(50, savings_account)
Argument Matchers
引数を厳密にチェックしたくないときに使えるMatcher。withメソッドの引数として使う。
instance_of
読んで字のごとく、引数に該当するインスタンスかどうかをチェックするMatcher。下記のように使う。
source_account.should_receive(:transfer).with(target_account, instance_of(Fixnum))
anything
引数があればその中身は何でもかまわないときに使う。
source_account.should_receive(:transfer).with(anything(), 50)
any_args
どんな引数が指定されていてもかまわない時に使う。
source_account.should_receive(:transfer).with(any_args())
no_args
引数を指定しないことを期待するときに使う。
collaborator.should_receive(:message).with(no_args())
hash_including
引数としてHashを期待していて、そのHashが含んでいるkey/valueを指定したいときに使う
mock_account.should_receive(:add_payment_accounts). with(hash_including('Electric' => '123' , 'Gas' => '234'))
hash_not_includeing
hash_including の反対。
mock_account.should_receive(:add_payment_accounts). with(hash_not_including('Electric' => '123' , 'Gas' => '234'))
Regular Expressions
Stringの引数は正規表現にマッチするかどうかのチェックが出来る。
mock_atm.should_receive(:login).with(/.* User/)
Custom Argument Matchers
Argument Matcherを自分で用意することも出来る。
たとえばある数値以上のFixnumを期待するとき、下記のように書けるようなmatcherを作るとする。
calculator.should_receive(:add).with(greater_than(3))
下記のように、==メソッドを定義したインスタンスを渡すようにすると実現できる。
class GreaterThanMatcher def initialize(expected) @expected = expected end def description "a number greater than #{@expected}" end def ==(actual) actual > @expected end end def greater_than(floor) GreaterThanMatcher.new(floor) end
Returning Consective Values
複数回実行するメソッドで、それぞれの実行時での戻り値を変えたいときは
mock.should_reveive(:hoge).and_return("hoge", "fuga", "foo")
のようにand_returnの引数に複数個の値を指定する。
Throwing or Raising
and_raiseメソッドで例外を返すdoubleを設定できる
account_double.should_receive(:withdraw).and_raise account_double.should_receive(:withdraw).and_raise(InsufficientFunds) the_exception = InsufficientFunds.new(:reason => :on_hold) account_double.should_receive(:withdraw).and_raise(the_exception)
and_throwでthrowすることもできる
account_double.should_receive(:withdraw).and_throw(:insufficient_funds)
and_yieldは説明が書いてなかったけど引数をブロック引数にしてブロックを実行するっぽい
account_double.should_receive(:balance).and_yield(args)
Ordering
doubleで定義したメソッドが呼ばれる順番を知りたいときがたまにある。そんなときはメソッド定義の後にorderedをくっつける。
describe Roster do it "asks database for count before adding" do database = double() student = double() database.should_receive(:count).with('Roster', :course_id => 37).ordered database.should_receive(:add).with(student).ordered roster = Roster.new(37, database) roster.register(student) end end
上記のような設定をした場合、count, addの順番でメソッドを実行しないとfailureになる
class Roster def initialize(id, database) @id = id @database = database end def register(student) @database.count('Roster', :course_id => @id) @database.add(student) end end
この設定はオブジェクト単位でしかできない。一つのオブジェクトに設定されたメソッドがどの順番で呼ばれるかしか見ることが出来ない。
orderedで設定されたメソッド以外が呼ばれてもそれは無視される。例えば下記のコードは beginとcommitが呼ばれているが count → add の順番に変更はないので問題なく pass される。
def register(student) @database.count('Roster', :course_id => @id) @database.begin @database.add(student) @database.commit end
Overrideing method stubs
beforeブロックでdoubleを作ってコードを綺麗にする例だった。省略。
14.7 When to Use Test Doubles and Test-Specific Extensions
doubleをいつ使ったらいいか。
- ネットワークやファイルシステムやらの外部との連携があって実装するのが大変なとき
- 例えばネットワークや外部サーバとの連携をしていて、外部の要因でspecがfailureになってしまう可能性があるとき
- サイコロのような実行するたびにランダム値が返ってくるようなものを使っているようなときも、そこをdoubleにすべき
- APIの設計はしてあるけど実装はまだできていないものに依存しているとき
- 実装中のオブジェクト以外のオブジェクトで、まだ存在しないけど、このメソッドがあったら便利だなというものを見つけたとき
- focus on role rather then object
- focus on interaction rather than state
14.8 Risks and Trade-Offs
doubleを使う時に気をつけること。
over-specification
一つのexampleにたくさんmockが必要だとセットアップが大変><。本当に必要なものだけやろう。多いなと思ったらデザインを見直そう。
Nested Doubles
doubleは簡単にするだけじゃなくて、「浅く」するべき。doubleを使って返ってくる値は単純な値になるのがベスト。doubleが返ってくるdouble(nested double)が必要になったときは、そもそもデザインが密結合でよくない可能性がある。
Absence of Coverage
mock側の実装を修正して、mockを使っている方のテストを修正していない場合、テストは通るけどエラーが出る。そうなるのは当たり前だけどどうにか回避したい。→より高いレベルの自動テスト(例:cucumber)を実行する