Rspec — 僕の知らなかったRspec
普段あまり使っていないものをまとめてみた。
subjectを書かない
超基本だが、普通に知らなかった。
describe に直接 subject の内容を書ける。
describe 1 do
it { is_expected.to eq 1 }
end
start_with / end_with マッチャ
始まりや終わりを検証できる。(どんな時に使うんだろう?)
describe "ABC" do
it { is_expected.to start_with 'A' }
it { is_expected.to end_with 'C' }
it { is_expected.not_to start_with 'X' }
it { is_expected.not_to end_with 'Z' }
# 確かに正規表現よりドキュメンタブルではある
it { is_expected.to match /^A/ }
it { is_expected.to match /B$/ }
it { is_expected.not_to match /^X/ }
it { is_expected.not_to match /Z$/ }
end
[In My Opinion]
配列の始まりや終わりも、同じマッチャで検証できるところがイイ。
describe 'DAY' do
subject { [:MORNING, :MIDDAY, :NIGHT] }
it { is_expected.to start_with :MORNING }
it { is_expected.to end_with :NIGHT }
end
対象が文字列だろうが配列だろうが関係ない。
「テストがドキュメンタブルであること」の一端があらわれていると思う。
これがもし start_string_with
/ start_array_with
とかいう名前だったら、検証表現ではなくてプログラミング的表現になってしまう気がする。
テストの表現にはあくまで「挙動」を記述したい、プログラミング内の状態を表現したいわけではない、という感じを受けた。
( match_array
とほぼ同じ動きをする contain_exactly
が出てきたのも「対象が配列かどうかなんて、関係ねーよ」っていう意図があるんじゃないだろうか )
eql マッチャ
eq は内部的に ==
での比較をおこなうが、eql は eql?
での比較をおこなう。
describe 1.0 do
# 1.0 == 1 は true
it { is_expected.to eq 1 }
# 1.0.eql? 1 は false
it { is_expected.not_to eql 1 }
end
参考: Ruby の文字列比較 [eql?, equal?, ==, ===] の使い分けを汗臭く説明 #ruby
be マッチャ
今まで eq
の エイリアスだと思っていたが、内部的には equal?
で比較をおこなうようだ。
describe do
# 同じ文字列は eq でマッチする
it { expect('ABC').to eq 'ABC' }
# 同じ文字列だが違うオブジェクトなので be ではマッチしない
it { expect('ABC').not_to be 'ABC' }
end
cover マッチャ
範囲の中に、値が含まれているかどうかのテストが出来る。
数値
describe (1..10) do
# 整数
it { is_expected.to cover 1 }
it { is_expected.to cover 10 }
# 小数点も受け付ける
it { is_expected.to cover 1.0 }
it { is_expected.to cover 10.0 }
it { is_expected.not_to cover 0.9 }
it { is_expected.not_to cover 10.1 }
end
日付
describe Time.now - 60 .. Time.now + 60 do
# 日付も検証できる
it { is_expected.to cover Time.now }
it { is_expected.not_to cover Time.now - 70 }
it { is_expected.not_to cover Time.now + 70 }
end
and / or
いくつかの条件を組み合わせて検証することが出来る。
describe 'ABC' do
# and
it { is_expected.to start_with('A').and include('B').and end_with('C') }
# or
it { is_expected.to eq('ABC').or eq('XYZ') }
end
あんまり複雑な条件では検証できない様子。
たとえば A and Not B
とか not A and B
とかいうのはサポートされていない。
Rspec も「使えそうなのに使えない」ということを心得ているのか、親切なメッセージを出してくれる。
describe 'ABC' do
it { is_expected.not_to start_with('X').and end_with('C') }
# NotImplementedError:
# `expect(...).not_to matcher.and matcher` is not supported, since it creates a bit of an ambiguity. Instead, define negated versions of whatever matchers you wish to negate with `RSpec::Matchers.define_negated_matcher` and use `expect(...).to matcher.and matcher`.
it { is_expected.to start_with('A').and.not_to end_with('X') }
# ArgumentError:
# wrong number of arguments (given 0, expected 1)
end
satisfy マッチャ
さてぃすふぁい。
ブロックに自分で判定を書ける! 自由度が高そう。
これを使えば、先程できなかった A and not B
みたいなマッチも、無理やり書けないことはない。(何の意味があるかは分からないが)
describe 'A' do
it { is_expected.to eq('A').and satisfy { |string| string != 'X' } }
end
マッチャの組み合わせ
マッチャの中にマッチャを書ける!
配列の順序
describe [[:A,:B], [:X, :Y]] do
# contain_exactly では配列の順番を考慮しない
# なので「外側の配列」の順番が違ってもマッチする
it do
is_expected.to contain_exactly(
[:X, :Y],
[:A, :B]
)
end
# この書き方では「内側の配列」の順序が違うとマッチしない
it do
is_expected.not_to contain_exactly(
[:Y, :X],
[:B, :A]
)
end
# マッチャを組み合わせれば「内側の配列」が順序が違ってもマッチする
# contain_exactly の中で、さらに contain_exactly でマッチをおこなう
it do
is_expected.to contain_exactly(
contain_exactly(:Y, :X),
contain_exactly(:B, :A)
)
end
# 「外側の配列」の順序は考慮し、だが「内側の配列」の順序は考慮しない場合
it do
is_expected.to match([
contain_exactly(:B, :A),
contain_exactly(:Y, :X)
])
end
# 「外側の配列」の順序は考慮せず、だが「外側の配列」の順序は考慮しない場合
it do
is_expected.to contain_exactly(
match([:X, :Y]),
match([:A, :B])
)
end
end
検証内容を組み立てる
Rspec って DSL っぽく見えるけど、Ruby的に色々と工夫してDSLっぽく見えるようにしてるだけ、という理解をして良いんだろうか。
つまりはメソッド+引数をうまく渡せば動いてしまう。
# 検証内容自体も、let で定義できる ( 良し悪しはさておき )
describe [1,2,3] do
describe do
# to の後に渡すのは要するに引数なので
# マッチャ名すら直接は書かなくても良いっぽい
# ただしまったくドキュメンタブルでなくなってしまうので、やめた方が良い
let(:expect_matches) { match([1,2,3]) }
it { is_expected.to(expect_matches) }
end
# let の中で マッチの内容を組み立てて、さらにそれをマッチャに渡す書き方
describe do
let(:expect_equals) { [eq(1),eq(2),eq(3)] }
it { is_expected.to match(expect_equals) }
end
end
have_attributes
オブジェクト同士の比較の問題
ActiveRecord のオブジェクト同士を比較した場合、プライマリーキーさえ同じなら true が返ってきてしまう。
Rspec を書く時、意外に落とし穴になるんじゃないだろうか。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'eq' do
subject { User.new(id: 1, name: 'Alice') }
# name は違うが id が同じなのでマッチしてしまう
it { is_expected.to eq User.new(id: 1, name: 'Bob') }
end
describe 'include' do
subject { [User.new(id: 1, name: 'Alice')] }
# name は違うが id が同じなのでマッチしてしまう
it { is_expected.to include User.new(id: 1, name: 'Bob') }
end
end
=> Ruby on Rails | ActiveRecord で オブジェクト同士を==で比較した場合、全属性が同値かどうかは検証しない
同値性を検証しよう
have_attributes ならオブジェクト自体ではなく、各属性の同値性を検証できる。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'have_attributes' do
subject { User.new(id: 1, first_name: 'Alice', last_name: 'Liddel') }
# キーを省略した場合は、それを無視してマッチを検証してくれる
# このため、一部のキーに対してだけの検証をおこなうことが出来る
# たとえば have_attributes に last_name を渡さない場合は、last_name の検証はおこなわない
it { is_expected.to have_attributes(id: 1) }
it { is_expected.to have_attributes(id: 1, first_name: 'Alice') }
it { is_expected.to have_attributes(id: 1, first_name: 'Alice', last_name: 'Liddel') }
# キーを渡し、なおかつ値が違う場合は、マッチは失敗する
# it { is_expected.to have_attributes(id: 1, first_name: 'Alice', last_name: 'Pleasance') }
# expected #<User id: 1, name: nil, created_at: nil, updated_at: nil, first_name: "Alice", last_name: "Liddel"> to have attributes {:id => 1, :first_name => "Alice", :last_name => "Pleasance"} but had attributes {:id => 1, :first_name => "Alice", :last_name => "Liddel"}
# Diff:
# @@ -1,4 +1,4 @@
# :first_name => "Alice",
# :id => 1,
# -:last_name => "Pleasance",
# +:last_name => "Liddel",
end
end
not_to have_attributes
の謎
先程書いた、この落ちるテスト。
# キーを渡し、なおかつ値が違う場合は、マッチは失敗する
it { is_expected.to have_attributes(id: 1, first_name: 'Alice', last_name: 'Pleasance') }
「to
で落ちるからには not_to
にすれば通るだろう」と思いきや、こちらも落ちてしまう。
# it { is_expected.not_to have_attributes(id: 1, first_name: 'Alice', last_name: 'Pleasance') }
# Failure/Error: it { is_expected.not_to have_attributes(id: 1, first_name: 'Alice', last_name: 'Pleasance') }
# expected #<User id: 1, name: nil, created_at: nil, updated_at: nil, first_name: "Alice", last_name: "Liddel"> not to have attributes {:id => 1, :first_name => "Alice", :last_name => "Pleasance"}
not_to have_attributes
のマッチでは、「記述した全ての値が異なる場合」にテストが通るっぽい。
「ひとつの値だけが違う」場合には、テスト自体が落ちるようだ。(分かりにくい)
RSpec.describe User, type: :model do
describe 'have_attributes' do
subject { User.new(id: 1, first_name: 'Alice', last_name: 'Liddel') }
# id と first_name と last_name が全て違うため、テストが通る
it { is_expected.not_to have_attributes(id: 2, first_name: 'Bob', last_name: 'Pleasance') }
# last_name が異なるが、id と first_name が同じなので、テストが落ちる
# it { is_expected.not_to have_attributes(id: 1, first_name: 'Alice', last_name: 'Pleasance') }
end
end
オブジェクトの配列に対して、(各属性の)同値性を検証したい
User.all
とかの内容を検証したい場合、 単純に have_attributes
は使えない。
かと言って contain_exactly
を使うと、オブジェクト同士の ==
での比較になってしまう。
この場合、動的にマッチャを組み立てる事もできる。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'multiple have_attributes' do
subject { User.all }
# 名字のないユーザーを生成する
let!(:users) do
[
User.create(first_name: 'Alice'),
User.create(first_name: 'Bob'),
User.create(first_name: 'Carol')
]
end
# 「全員分のフルネーム」を期待するマッチャのリストを作る
let(:expect_full_names) do
users.map do |user|
have_attributes(first_name: user.first_name, last_name: 'Liddel')
end
end
# expect_full_names には、Rspec のマッチ用オブジェクトが配列に収められた状態になる
# => [#<RSpec::Matchers::BuiltIn::HaveAttributes:0x007f9b0fef9690
# @expected={:first_name=>"Alice", :last_name=>"Liddel"},
# @negated=false,
# @respond_to_failed=false,
# @values={}>,
# #<RSpec::Matchers::BuiltIn::HaveAttributes:0x007f9b0fef9528
# @expected={:first_name=>"Bob", :last_name=>"Liddel"},
# ..
# 全員に名字を付ける
before do
User.update_all(last_name: 'Liddel')
end
# 動的に生成したマッチオブジェクトを、さらにマッチャに与える
# こうして「オブジェクトの配列」全体を検証する
it { is_expected.to contain_exactly(*expect_full_names) }
end
end
ただマッチ内容自体を let
で作るのは気持ち悪いので、実際にはマッチャの中で直接展開した方が良いかもしれない。
it do
is_expected.to contain_exactly(*
users.map do |user|
have_attributes(first_name: user.first_name, last_name: 'Liddel')
end
)
end
include + have_attributes
合わせて使える。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'include have_attributes' do
subject { User.all }
# みんなで遊ぼう
let!(:users) do
[
User.create(first_name: 'Alice'),
User.create(first_name: 'Bob'),
User.create(first_name: 'Carol'),
User.create(first_name: 'Dave')
]
end
# ボブとキャロルはいるよ
it 'includes' do
is_expected.to include(
have_attributes(first_name: 'Bob'),
have_attributes(first_name: 'Carol')
)
end
# エリックとフランクはいないよ
it 'does not include' do
is_expected.not_to include(
have_attributes(first_name: 'Eric'),
have_attributes(first_name: 'Frank')
)
end
# アリスはいるけどエリックはないよ ( FAILURE )
# expected #<ActiveRecord::Relation [#<User id: 1, name: nil, created_at: "2017-02-14 06:16:39", updated_at: "20..._at: "2017-02-14 06:16:39", updated_at: "2017-02-14 06:16:39", first_name: "Dave", last_name: nil>]> to include (have attributes {:first_name => "Eric"})
# Diff:
# -[(have attributes {:first_name => "Alice"}),
# - (have attributes {:first_name => "Eric"})]
#
# it 'includes' do
# is_expected.to include(
# have_attributes(first_name: 'Alice'),
# have_attributes(first_name: 'Eric')
# )
# end
# エリックはいないけどアリスはいるよ ( FAILURE )
# expected #<ActiveRecord::Relation [#<User id: 1, name: nil, created_at: "2017-02-14 06:16:39", updated_at: "20..._at: "2017-02-14 06:16:39", updated_at: "2017-02-14 06:16:39", first_name: "Dave", last_name: nil>]> not to include (have attributes {:first_name => "Alice"})
#
# Diff:
# -[(have attributes {:first_name => "Alice"),
# - (have attributes {:first_name => "Eric"})] it 'does not include' do
# it 'does not include' do
# is_expected.not_to include(
# have_attributes(first_name: 'Alice'),
# have_attributes(first_name: 'Eric')
# )
end
end
end
柔軟な have_attributes
ちなみにこの have_attributes はちょっと柔軟に出来ていて、同値検証以外のことも出来る。
describe 'progressive have_attributes' do
subject { User.new(first_name: 'Alice') }
it { is_expected.to have_attributes(first_name: start_with('A')) }
it { is_expected.to have_attributes(first_name: include('lic')) }
it { is_expected.to have_attributes(first_name: end_with('e')) }
# have_attributes の中でも and / or 条件を書ける
it do
is_expected.to have_attributes(
first_name: (start_with('A').and include('lic').and end_with('e'))
)
end
end
end
環境
- rspec-rails (3.5.2)
- Rails 5.0.1
参考
- https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations
- https://relishapp.com/rspec/rspec-expectations/docs/define-negated-matcher
- https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/have-attributes-matcher
- http://qiita.com/tbpgr/items/a1f231999910cd48ec58
チャットメンバー募集
何か質問、悩み事、相談などあればLINEオープンチャットもご利用ください。
公開日時
2017-02-14
Discussion