Edited at

rspec-givenですっきりしたspecを書く

More than 3 years have passed since last update.

社内のプロダクトで一部rspec-givenを使い始めたので、ざっと説明を書いておこうかなと。

追記

もう少し細かく、評価タイミングやこう使ったらいいのではないかという方法をまとめました

ワンライナーなspecを書きやすくするrspec-givenをもっといい感じに使う


rspec-givenとは


Given/When/Then keywords for RSpec Specifications


rspec-givenのページに書かれている通り、Given, When, Then などを使ってCucumberライクにspecを書けるextensionです。


rspec-givenを使う理由

すっきりきれいに書ける(と思う)からです。

なんかsubject, let, itはなんかピンと来ない部分がありますが、Given, When, Thenと並ぶと分かりやすいです。

Given, When, Thenの他にもNatural Assertions, Andキーワードなど自然な感じに書けるために色々頑張ってて非常に使いやすいです。

CucumberライクでもあるのでCucumber好きならなお好むところかもしれません。(僕はCucumberあまり使ってないのでわかりませんが)

例えばこういうスペックが

subject(:user) { User.new(first_name: first_name, last_name: last_name) }

let(:first_name) { 'Yure' }
let(:last_name) { 'Shinatose' }
it { expect(user.full_name).to eq "#{first_name} #{last_name}" }

こうなります。

subject(:user) { User.new(first_name: first_name, last_name: last_name) }

Given(:first_name) { 'Yure' }
Given(:last_name) { 'Shinatose' }
Then { user.full_name == "#{first_name} #{last_name}" }

ちなみに、rspec3ではitsが削除されることになっていて、以下の様なワンライナーはrspec-itsなどを使わないと書けなくなっています。

subject(:user) { User.new(first_name: first_name, last_name: last_name) }

let(:first_name) { 'Yure' }
let(:last_name) { 'Shinatose' }
its(:full_name) { is_expected to eq("#{first_name} #{last_name}") }

itsが削除される理由はExplanation for why its will be removed from rspec-3で記されてますが、その中でワンライナーで書くとしたらと勧めているのがrspec-givenです。


rspec-givenを使った書き方

以下を例にすると

Given(:user) { User.new(first_name: first_name, last_name: last_name) }

When(:first_name) { 'Yure' }
When(:last_name) { 'Shinatose' }
Then { user.full_name == "#{first_name} #{last_name}" }

Givenがrspecのsubjectに当たるキーワードで、Thenの中などで値が期待通りかチェックする対象を指定します。ここではuserを指定していて、Thenの中のuserが呼ばれたタイミングで実行されます。

ちなみにGiven(:user) { ... }Given { ... }と書くとThen毎に実行されます。

Whenbeforeに当たるもので、「◯◯の場合」のような条件を示します。ここではfirst_namelast_nameの値を設定していて、それぞれが呼ばれたタイミングで実行されます。

Thenitに当たり、expectationを書く部分です。このThenの中ではNatural Assertionを使ってexpectationに当たるものを書いてます。

またAndというものもあって、Thenと似た同じ動きをします。

ただ、Thenで評価されたものを使いまわします。下の例ではThenで評価しているuserを再度評価することはないです。

Thenを続けて書くよりThen, And, And...と書きたい時に使えます。

Then { user.first_name == first_name }

And { user.last_name == last_name }
And { usre.full_name == "#{first_name} #{last_name}" }


Natural Assertion

Given, When, Thenのキーワードが使えるのが一番のポイントですが、実はNatural Assertionも非常に強力なフィーチャーだと思います。

Natural Assertionというのは要は==を使って自然な感じのアサーションを書けるということです。

またfailした時に詳細なレポートが出てくるのも嬉しいです。

例えばrspec標準のrspec-expectationsはこういう書き方になりますが

expect(user.full_name).to eq "#{first_name} #{last_name}")

rspec-givenではこういう書き方を出来るようになっています。

user.full_name == "#{first_name} #{last_name}"

またspecがfailした時にはこんな感じに詳細なレポートがでます。

### 実行結果 ###

User
#full_name
Then { full_name == "#{first_name} #{last_name}" } (FAILED - 1)

Failures:

1) User#full_name Then { full_name == "#{first_name} #{last_name}" }
Failure/Error: Then { full_name == "#{first_name} #{last_name}" }
Then expression failed at /path/to/spec/test.rb:24
expected: nil
to equal: "Nagate Tanikaze"
false <- full_name == "#{first_name} #{last_name}"
nil <- full_name
"Nagate" <- first_name
"Tanikaze" <- last_name
# ./spec/test.rb:24:in `block in Then'

Finished in 0.00136 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/test.rb:24 # User#full_name Then { full_name == "#{first_name} #{last_name}" }

ちょっと慣れないと読みにくいですが、結果がfalseでfull_nameがnilだと分かり、Whenで渡されたfirst_nameとlast_nameの値も表示されます。

普通にexpectで書くと以下のようにletで設定した値がどうだったか等は表示されません。

### 実行結果 ###

Failures:

1) User#full_name should eq "Yure Shinatose"
Failure/Error: it { expect(user.full_name).to eq "#{first_name} #{last_name}" }

expected: "Yure Shinatose"
got: nil

(compared using ==)
# ./spec/test.rb:21:in `block (3 levels) in <top (required)>'

Natural Assertion周りは他にもFailureやfuzzy number matchingなど色んなマッチャーが準備されているので使ってみると面白いです。


rspec-givenな書き方が出来ない時は

Invariantなどその他全部の機能を駆使すればほとんどrspec-givenな書き方で書けると思いますが、うまく書き方が浮かばない時もあります。

rspec-givenを使っていても普通にsubject, let, itも使えるし、expectも使えるので深く考えないで普通に今までどおりのrspecの書き方で書いてしまってもいいと思います。

rspec-givenのREADMEの中でもrspecと混成した書き方を示してます