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両方で使ったときは
- 外側のbefore
- 内側のbefore
- example
- 内側のafter
- 外側の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だったらもっと必要。つまりバランスが大事。正解はない。