読者です 読者をやめる 読者になる 読者になる

maeshimaの日記

メモ書きです

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は少なめにするよう気をつけた方がいい。多くするとうまくいかなくなる。

14.5 More on Method Stub

Spec::MocksのAPIkwskみていく。

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)を実行する

Brittle examples

mock作るのがしんどい→設計に問題ありかも。なるべく疎結合APIにすべき

14.9 Choosing other Test Double Frameworks

Rspec用のアダプタをもってるdouble frameworkであればRspecで使える。

  • Mocha
  • Flex-mock
  • RR

この三つが有名。アダプタの作り方あるけど省略。

一度に使うのは一つのフレームワークにしておかないとエラーになるかも。たとえばmockやstubのメソッドはRSpecとMochaで共通してるので両方使おうとすると変なことになる。