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

おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

the rspec book - Chapter 12 Spec::Example その2

rspec rails 一人読書会

the rspec book - Chapter 12 Spec::Example その1 - おもしろWEBサービス開発日記の続きです。

before メソッド

before(:all)とbefore(:each)がある。それぞれ

  • before(:all)はexample groupで一回
  • before(:each)はexample毎に一回

実行される。初期状態ごとにグループ化するのに使う。

通常はbefore(:each)を使ってexample毎に初期状態を作り直した方がいい。そうしないと別のexampleに移行するときに状況が変になっちゃうかもしれないので。

before(:all)を使う時は気をつけるべき。適当に使うとバグの温床になりかねない。ネットワークの接続をopenするとかの時にbefore(:all)を使うといい。通常、それぞれ分離されているexampleではbefore(:all)は使わない。

after メソッド

after(:each)はあんまり使わない。なぜなら、それぞれのexampleは独自のスコープで実行されて、exampleの実行後にインスタンス変数はスコープ外になるから。でも場合によってはとても使える。

例えば、あるexampleだけglobalな設定を修正して使いたいけど基本的にはそのままにしておく必要がある場合、下記のようにbefore(:each)でglobal stateを修正してafter(:each)で元に戻すことでうまいことできる。

before(:each) do
  @original_global_value = $some_global_value
  $some_global_value = temporary_value
end
after(:each) do
  $some_global_value = @original_global_value
end

after(:each)は、exampleがfailureだったりエラーが起こっても必ず実行される。

after(:all)はafter(:each)よりもさらに使われることがまれだけど使うのが妥当なときがある。

  • ブラウザを閉じる
  • データベースのコネクションを閉じる
  • socketsを閉じる

などなど、基本的になにかを確実にシャットダウンさせたいときに使う。

beforeやafterを使うことで、テストがDRYになるだけじゃなくてexampleが理解しやすくなる。

Helper Methods

example group内でメソッド定義してDRYに。

ブロックを受け付けるヘルパーメソッドを作って、コードを見やすくする例

describe Thing do
  def given_thing_with(options)
    yield Thing.new
      do |thing| thing.set_status(options[:status])
    end
  end
  it "should do something when ok" do
    given_thing_with(:status => 'ok') do |thing|
      thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
      # ...
    end
  end
  it "should do something else when not so good" do
    given_thing_with(:status => 'not so good') do |thing|
      thing.do_fancy_stuff(1, true, :move => 'left', :obstacles => nil)
      # ...
    end
  end
end

上記のようにブロックを使うと見やすくなる。これを使う使わないは個人の好み。given_thing_withメソッドを理解するために別の場所のソースを読む必要があるというのが欠点。この種類の間接的な手法は使いすぎると良くない。それぞれのコードベースで一貫性を保つことが大事。

Sharing Helper Methods

ヘルパーメソッド複数のexample groupで共有したいときは、moduleとして定義して、describeブロックのなかでincludeする。すべてのexample groupの中で使いたいmoduleがあるときには、configファイルでincludeする。

Shared Examples

複数のクラスで完全に同じ振る舞いをする時に、shared example groupが使える。一回定義したら他のexample groupでinclude出来る。

下記のように、shared_examples_forで宣言する。

shared_examples_for "Any Pizza" do
  it "should taste really good" do
    @pizza.should taste_really_good
  end
  it "should be available by the slice" do
    @pizza.should be_available_by_the_slice
  end
end

it_should_behave_likeでincludeする

describe "New York style thin crust pizza" do
  it_should_behave_like "Any Pizza"
  before(:each) do
    @pizza = Pizza.new(:region => 'New York', :style => 'thin crust')
  end
  it "should have a really great sauce" do
    @pizza.should have_a_really_great_sauce
  end
end
describe "Chicago style stuffed pizza" do
  it_should_behave_like "Any Pizza"
  before(:each) do
    @pizza = Pizza.new(:region => 'Chicago', :style => 'stuffed')
  end
  it "should have a ton of cheese" do
    @pizza.should have_a_ton_of_cheese
  end
end

上記の例だと@pizzaが参照されてるのはbeforeの前だけど、shared examplesはbeforeが実行されてから実行されるので大丈夫。

sharing Examples in a Module

share_examples_forとit_should_behave_likeと同じことが、share_asメソッドとincludeでできる。

share_as :AnyPizza do
  # ...
end
describe "New York style thin crust pizza" do
  include AnyPizza
  # ...
end
describe "Chicago style stuffed pizza" do
  include AnyPizza
  # ...
end

どっちをつかうにせよshared exampleはとても使いどころが限られている。ほかのexampleと状態を共有するにはインスタンス変数を共有するくらいしかできない。it_should_behave_likeメソッドでは状態をグループに渡すことが出来ない。

この制限によって、shared exampleは使いどころが限られる。もっとがっつりやりたいならカスタムマクロなるものを作るといいらしい。

Nested Example Groups

example groupのネストっていうのはこういうこと。

describe "outer" do
  describe "inner" do
  end
end

外側のgroupはExampleGroupのサブクラス。内側のgroupは外側のgroupのサブクラス。なのでヘルパーメソッドやbefore, after, includeしたモジュールなど、外側のgroupで宣言したものは内側のgroupでも使える。

beforeやafterを内側、外側のgroup両方で使ったときは

  1. 外側のbefore
  2. 内側のbefore
  3. example
  4. 内側のafter
  5. 外側のafter

の順番で実行される。

ネストされたexample groupは同じオブジェクトのコンテキストで実行されるので、beforeブロックで設定した状態は複数のexampleでシェアできる。つまり段階的なsetupができる。具体的には下記のように外側のgroupで前提を設定して、内側のgroupでイベントを設定するみたいな感じのことができる。

describe Stack do
  before(:each) do
    @stack = Stack.new(:capacity => 10)
  end
  describe "when full" do
    before(:each) do
      (1..10).each {|n| @stack.push n}
    end
    describe "when it receives push" do
      it "should raise an error" do
        lambda { @stack.push 11 }.should raise_error(StackOverflowError)
      end
    end
  end
  describe "when almost full (one less than capacity)"
    before(:each) do
      (1..9).each {|n| @stack.push n}
    end
    describe "when it receives push" do
      it "should be full" do
        @stack.push 10 @stack.should be_full
      end
    end
  end
end

上記のコードを見て、だれかが " DRYだぜ! " という一方で " 可読性下がってね? " という人もいると思う。個人的には後者に賛成で、なるべくこういう構造にするのは避けた方がいい。理由はfailureな理由を理解するのが大変になるから。でも最終的に何が自分に適しているかを見つける必要があるし、こうすることも出来るよというのを知って欲しかった。注意して使って欲しい。

私はnested example groupはいつも使ってる。私はコンセプトををまとめるのにネストを使っていて、状態を作るのには使ってない。例えば上記の例を私ならこんな風に書く。

describe Stack do 
  describe "when full" do
    before(:each) do
      @stack = Stack.new(:capacity => 10)
      (1..10).each {|n| @stack.push n}
    end
    describe "when it receives push" do
      it "should raise an error" do
        lambda { @stack.push 11 }.should raise_error(StackOverflowError)
      end
    end
  end
  describe "when almost full (one less than capacity)"
    before(:each) do
      @stack = Stack.new(:capacity => 10) (1..9).each {|n| @stack.push n}
    end
    describe "when it receives push" do
      it "should be full" do
        @stack.push 10 @stack.should be_full
      end
    end
  end
end

beforeは全然使わないという方法もある。

describe Stack do 
  describe "when full" do
    describe "when it receives push" do
      it "should raise an error" do
        stack = Stack.new(:capacity => 10) (1..10).each {|n| stack.push n}
        lambda { stack.push 11 }.should raise_error(StackOverflowError)
      end
    end
  end
  describe "when almost full (one less than capacity)"
    describe "when it receives push" do
      it "should be full" do
        stack = Stack.new(:capacity => 10) (1..9).each {|n| stack.push n}
        stack.push 10
        stack.should be_full
      end
    end
  end 
end

上記の例はたぶん三つの例の中で一番読みやすい。ネストされたdescribeのブロックは概念とドキュメントを結合したものを提供する。この方法のいいところは、failureになったときに一カ所だけ見ればいいこと。

悪い点は、三つの例の中で一番DRYでないこと。もしStackのコンストラクタを変更するとしたら、二カ所変更する必要がある。完璧なexampleだったらもっと必要。つまりバランスが大事。正解はない。