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

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

Chapter 13 Spec::Expectations

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

の続き。

BDD

BDDのゴールの一つは正しい言葉を得ること。技術的なことのわからない人ともコミュニケーションをとれるようにがんばってるところ。なのでGivenとかWhenとかThenとかを使ってる。assertionの代わりにexpectationsについて語る。"to assert"は辞書の定義では"事実を述べる、または自信を持て信じる"という意味。これは法廷でなんかするみたいな感じでナンセンス。

13.1 should, should_not, and matchers

Rspecはshouldメソッドと,should_notメソッドをObjectに追加してる。これらのメソッドは matcher か演算子を受け付ける。matcherとは、期待する結果にマッチしてるか確かめるオブジェクトのこと。

expectationの処理の流れ

result.should equal(5)

1. 先にequal(5)が処理され、「5に等しい」という設定のmatcher objectが返り、result.shouldの引数になる。
2. self.should(matcher) は matcher.matches?(self) を呼び出す。
3-1 matches?(self)がtrueならexampleの次の行に移動する。
3-2 falseならmatcher.failure_message_for_shouldを付けてExpectationNotMetErrorを投げる。

shouldメソッドは全てのオブジェクトにも追加されているので、selfはどんなオブジェクトにもなり得る。同じように、matches?が定義されている全てのオブジェクトはmatcherになり得る。

13.2 Built-In Matchers

RSpecが標準で定義しているMatcherがいろいろある。下記は使用例

prime_numbers.should_not include(8) 
list.should respond_to(:length) 
lambda { Object.new.explode! }.should raise_error(NameError)

equality: Object Equivalence and Object Identity

rubyで値が同じか調べる演算子は四つある。

  • a == b
  • a === b
  • a.eql? b
  • a.equal? b

これらは状況によって意味が異なるので混乱しやすい。RSpecは状況によらない厳密なメソッドを提供してる

  • a.should == b
  • a.should === b
  • a.should eql(b)
  • a.should equal(b)

== が一番よく使われる。== は値の同一性を調べる。オブジェクトの同一性を調べる場合は下記のようにする。

person = Person.create!(:name => "David")
Person.find_by_name("David").should equal(person)

キャッシュを期待しているときにはオブジェクトの同一性をチェックする方が適しているかもしれないけど、制限を強くするとその分expectationがもろくなりやすい。もしキャッシュを求めてないなら==で十分。そうするとコードをリファクタリングしたときにfailになる可能性が少なくなる。

Do not user !=

!= はサポートしてないので should_not をつかうべし。なんで != がサポートされてないかというと、Ruby的には==はメソッドで、!=は==の結果を反転させたもの。なので

actual.should != expected

!(actual.should.==(expected))

と解釈されてしまう。!=でも==でもshouldで受け取るのは==になってしまう。RSpec的には!=なのか==なのかわからないので、間違ったレスポンスを受け取ることになる。

Floating Point Calculations

floatの計算結果をexpectationにするのはめんどい。小数点第二位まででいいときに下記のようになったりする。

expected 5.25, got 5.251

これを解決するために、RSpecは be_close matcherを定義している。

result.should be_close(5.25, 0.005)

とすると, resultが0.005から0.25までならパスする。

Mutiline Text

文字列を生成するオブジェクトを作ってるとする。それを比較するexampleを下記のような感じで仮に作ったとする。

expected = File.open('expected_statement.txt','r') do |f|
  f.read
end
account.statement.should == expected

このやり方は高いレベルのcode exampleにはとても良いけど、もっと小さいexampleでやるにはしんどいし、うまくいかないときの問題を切り分けが難しくなる。

また、文字列の全体まで見ないで一部だけ見たいときがある。特定の書き方で書かれているか見たいだけで、詳細までは見たくない時もある。少しの詳細だけ見たくて書き方までは見たくないときもある。それらの全てのケースで下記のような正規表現を使える。

result.should match(/this expression/)
result.should =~ /this expression/

複数行の場合は下記のように出来る

statement.should =~ /Total Due: \$37\.42/m

このアプローチの利点は、関係ない部分の変更で失敗しにくいところ。RSpec自身のexamplesはこのような感じの、エラーメッセージに関したexmapleで占められている。

ch,ch,ch,ch,changes

Rails は Test::Unit をrailsに特化した assertions で拡張してる。その一例が assert_difference()で、これはレコードをデータベースに insert したときによく使われる。

assert_difference 'User.admins.count', 1 do
  User.create!(:role => "admin")
end

これはブロックを執行した後に User.admins.count が1増えることをassertしてる。Rspecだとこう書く

lambda {
  User.create!(:role => "admin")
}.should change{ User.admins.count }

by(), to(), from()を繋げて使うことにより, より明示的に書くことも出来る。

lambda {
  User.create!(:role => "admin")
}.should change{ User.admins.count }.by(1)

lambda {
  User.create!(:role => "admin")
}.should change{ User.admins.count }.to(1)

lambda {
  User.create!(:role => "admin")
}.should change{ User.admins.count }.from(0).to(1)

これはRails以外でも使える。不動産屋が250,000ドルの売り上げから7500ドルの成約料をゲットするのを書いてみるとこうなる

lambda {
  seller.accept Offer.new(250_000)
}.should change{agent.commission}.by(7_500)

下記のように明示的に、始めと終わりの期待する値を書くことも出来る。

agent.commission.should == 0
seller.accept Offer.new(250_000)
agent.commission.should == 7_500

この直訳的な書き方は一目見てわかりやすいかもしれない。でも should change を使う方法は何の出力を期待するのかを特定するのに便利。

from()とto()を上記のexample上に使った場合、二つ以上のexpectationのラッパーとして機能する。


上記のどのアプローチを選ぶべきか。実際のところ個人のスタイルと好み次第。一人でやってるなら好きにしていいし、グループでやってるなら話し合って決めた方がいい。

Expecting Errors

raise_errorマッチャについて

lambda {
  account.withdraw 75, :dollars
}.should raise_error(
  "attempted to withdraw 75 dollars from an account with 50 dollars"
)

raise_error マッチャは0から2個の引数を撮る。

  • 引数をとらなければ、Exceptionのサブクラスを補足する。
  • 第一引数がStringまたはRegexpだったら、正規表現で実際のエラーメッセージとマッチするかどうかを調べる。
  • 第一引数がerrorクラスなら、第二引数にStringまたはRegexpをとれる。

このうちどの形式を選ぶかは、どのように型やメッセージを特定したいかによる。

Expecting a Throw

try catchのspec throw_symbol

course = Course.new(:seats => 20)
20.times { course.register Student.new }
lambda {
  course.register Student.new
}.should throw_symbol(:course_full)

引数はraise_errorと同じように0から2つとる。

  • 引数がない場合はthrowされた全ての場合にパスする。
  • 一つ目の引数はシンボルでなければいけない。
  • 二つ目の引数はthrowされたオブジェクトを指定する。
course = Course.new(:seats => 20)
20.times { course.register Student.new }
lambda {
  course.register Student.new
}.should throw_symbol(:course_full, 20)