概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Cleaner RSpec examples and failure messages
- 原文公開日: 2015/12/25
- 著者: Jeroen Weeink
- サイト: Crafting Ruby
RSpecのexampleや失敗時のメッセージをわかりやすくするコツ(翻訳)
RSpecは、テストを書くための多方面に渡る柔軟なDSLを提供します。しかしマッチャーが実に多岐に渡っているため、Minitestのようなテストフレームワークに比べるとマッチャーを正しく使うために頑張りが必要になります。
次のspecをご覧ください。
RSpec.describe ShoppingCart, type: :model do
let(:shopping_cart) { subject }
describe '#to_order' do
let(:product1) { create(:product, price: 10) }
let(:product2) { create(:product, price: 3) }
let(:user) { create(:user) }
before do
shopping_cart.insert(product1, quantity: 2)
shopping_cart.insert(product2)
end
it '正しい属性を持つ' do
order = shopping_cart.to_order(user)
expect(order.price).to eq(23)
expect(order.user).to eq(user)
end
it 'そのorderにproductsを追加する' do
line_items = shopping_cart.to_order(user).line_items
expect(line_items[0].product).to eq(product1)
expect(line_items[0].quantity).to eq(2)
expect(line_items[1].product).to eq(product2)
expect(line_items[1].quantity).to eq(1)
end
end
end
これは使えると言えば使えますが、とても読みやすいとまでは言えません。特にローカル変数の代入のあたりが、どうもRSpecらしい書き方には見えません。最後に、末尾のexampleで行のitem順を特定の方法でソートしています。不要な詳細実装を副作用として強いている点がまったく好きになれません。
組み込みマッチャー
RSpecには、expectationで使える多数組み込みマッチャーがひととおり用意されています。RSpecのドキュメントで組み込みマッチャーやargument matchersについて詳しく解説されています。
これらのマッチャーはさまざまな状況で利用できます。たとえばhave_attributes
やそのエイリアスであるan_object_having_attributes
もそうしたマッチャーのひとつです。
RSpec.describe ShoppingCart, type: :model do
let(:shopping_cart) { subject }
describe '#to_order' do
let(:product1) { create(:product, price: 10) }
let(:product2) { create(:product, price: 3) }
let(:user) { create(:user) }
before do
shopping_cart.insert(product1, quantity: 2)
shopping_cart.insert(product2)
end
it '正しい属性を持つ' do
expect(shopping_cart.to_order(user))
.to have_attributes(price: 23, user: user)
end
it 'そのorderにproductsを追加する' do
expect(shopping_cart.to_order(user).line_items).to contain_exactly(
an_object_having_attributes(product: product1, quantity: 2),
an_object_having_attributes(product: product2, quantity: 1)
)
end
end
end
contain_exactly
のおかげで、行itemのソート順に影響されなくなりました。
適切なマッチャーを使えば、specがまるで自然言語であるかのように読み取れます。exampleの内容を把握するために理解が必要となるロジックはそれほどありません。
RSpec 3で利用できるマッチャーとエイリアス一覧も便利なリソースです。
失敗時のメッセージをわかりやすくする
have_attributes
のようなマッチャーで非常に残念なのは、ActiveRecordオブジェクトと組み合わせたときに、1件の失敗メッセージに数個分のオブジェクトでもたちまち画面がいっぱいになってしまう点です。
Failures:
1) ShoppingCart#to_order adds the products to the order
Failure/Error: expect(shopping_cart.to_order(user).line_items).to contain_exactly(
expected collection contained: [(an object having attributes {:product => #<Product id: 1, name: "Product #3", price: 10, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">, :quantity => 2}), (an object having attributes {:product => #<Product id: 2, name: "Product #4", price: 3, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">, :quantity => 1})]
actual collection contained: [#<LineItem id: 1, product_id: 1, order_id: 1, quantity: 1, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">, #<LineItem id: 2, product_id: 2, order_id: 1, quantity: 1, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">]
the missing elements were: [(an object having attributes {:product => #<Product id: 1, name: "Product #3", price: 10, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">, :quantity => 2})]
the extra elements were: [#<LineItem id: 1, product_id: 1, order_id: 1, quantity: 1, created_at: "2015-12-14 22:42:05", updated_at: "2015-12-14 22:42:05">]
# ./spec/models/shopping_cart_spec.rb:22:in `block (3 levels) in <top (required)>'
こんなスパムまがいの大漁節メッセージを出力されても、どうでもよい情報がほとんどで肝心な部分が埋もれるだけなので馬鹿馬鹿しい限りです。
ひとつの方法は、失敗時に出力されるメッセージがもっとシンプルになるよう、次のような感じでオブジェクトに手を加えることです。
it 'adds the products to the order' do
line_items = shopping_cart.to_order(user).line_items.map do |item|
item.slice(:product, :quantity)
end
expect(line_items).to contain_exactly(
{ 'product' => product1, 'quantity' => 2 },
{ 'product' => product2, 'quantity' => 1 }
)
end
私はこのアプローチはまったく好きになれません。失敗メッセージはシンプルになるものの、specにロジックが追加されて複雑になってしまっています。このspecに目を通すたびに、ロジックをがっつり読み解いてexampleを理解する必要があります。
specの簡潔さを損なわずにオブジェクトの出力をカスタマイズするのは、実は割と簡単です。単にinspect
メソッドをオーバーライドするだけで、失敗メッセージ内のオブジェクトからの出力を変更できます。
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :order
def inspect
"#{quantity} x #{product.inspect}"
end
end
class Product < ActiveRecord::Base
def inspect
name
end
end
無関係なノイズを除去したことで、失敗メッセージが読みやすくなりました。
Failures:
1) ShoppingCart#to_order adds the products to the order
Failure/Error: expect(shopping_cart.to_order(user).line_items).to contain_exactly(
expected collection contained: [(an object having attributes {:product => Product #3, :quantity => 2}), (an object having attributes {:product => Product #4, :quantity => 1})]
actual collection contained: [1 x Product #3, 1 x Product #4]
the missing elements were: [(an object having attributes {:product => Product #3, :quantity => 2})]
the extra elements were: [1 x Product #3]
# ./spec/models/shopping_cart_spec.rb:22:in `block (3 levels) in <top (required)>'
inspect
の出力をカスタマイズするとpp
の出力がオーバーライドされるので、pp
によるオブジェクトのinspectを普段から多用しているのであれば、このアプローチは不便かもしれません。
class SomeObject
def inspect
'Test'
end
def pretty_print(pp)
pp.pp_object(self)
end
end
しかし、ActiveRecordオブジェクトのからの出力はあまりイケていません。これについてうまい方法がまだありませんが、私が見つけた中でベストなのは、privateメソッドをオーバーライドしてActiveRecordの出力を先ほどのような見やすい出力に変える方法です。
class SomeObject < ActiveRecord::Base
def inspect
'Test'
end
private
def custom_inspect_method_defined?
false
end
end
まとめ
RSpecが提供するマッチャーについて詳しく知っておけば、クリーンで簡潔でエレガントなexampleをかけるようになります。たまにはRSpecで使えるマッチャーのリストをざっと眺めてみる価値はあります。exampleを書いたら、一歩下がってもっとよい書き方はないかと考えてみることです。これを繰り返すうちに、クリーンなspecを書くノウハウを貯めることができ、ひいては複雑さを軽減できるようになります。
本記事で用いた完全なコードはGitHubのivedigit/rspec_matchersでご覧いただけます。
皆さんの好きなRSpecマッチャーがありましたら、ぜひ(原文)末尾のコメントにてお知らせください。