Posted at

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

More than 3 years have passed since last update.


もっといい感じにspecを書きたい

以前にrspec-givenについてこのような記事を書いたんですが、それなりに長い期間使っててより良さ気な使い方や、勘違いだったことなどもあり、もっといい感じに使うならこうしようと考えたものを書いておきます。

rspec-givenの紹介についてはこちらをお読みください。

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

以下、ところどころrspec-givenのドキュメントから引用しつつ書いていきます。


Givenはletと同じように使う

例えばこういうコードがあった場合について

Given(:stack) { Stack.new }


The block for the given clause is lazily run and its value bound to 'stack' if 'stack' is ever referenced in the test.


こうある通り、要はletと同じ動きです。

遅延評価をするので呼ばれるまでブロックは実行されません。

これは何かのインスタンスメソッドについて書く時に重宝します。

多少雑なspecですがこういう風にarrayを渡すのに使いやすいです。

describe '#pop' do

# array.popについてテストする
subject { array.pop }

# arrayがこういうものの場合
Given(:array) { [1, 2, 3] }

# 結果はこうなる
Then { subject == 3 }
end

なので、rspec-givenを使ってるならletは使わないようにしています。

同じ結果同じ意図で、違う記法のものはない方がいいかと思うので。


Given!は使わない

Given!については


The code block is run unconditionally once before each test


というように書いてあるのですが、結局動作がWhenと同じになってしまうので特に理由がない限りは使わない方が良いと思います。いくつもの記法があると混乱のもとになりますし。

Whenを使うようにしましょう。


Whenはbefore(:each)みたいなもの


The code block is executed once per test.


基本的にbefore(:each)と同じように動き同じタイミングで実行されます。

beforeと違うのはWhen(:company) { create(:company) }のように書けるので、companyをテストの中で使えたりすることです。

beforeのように@companyなどとインスタンス変数を作らなくてもいいのは読みやすくなってとても良いです。

ただ、Whenは上から実行していくのでWhenを大量に書くとどれが実行されたか分かりにくくなりやすいです。これはbefore(:each)がたくさんあるspecが読みにくいのと同じ理由ですね。

Given { ... }と書いてもWhenと同様に各テスト毎に動くんですが特に使う理由はない気がしてます。

Whenはcontextと組み合わせて「必ず実行したい条件」を書くときれいになりやすいです。

describe '#pop' do

subject { stack.pop }

context 'when stack is not empty' do
When(:stack) { [1, 2, 3] }
Then { subject == 3 }
end

context 'when stack is empty' do
When(:stack) { [] }
Then { subject == nil }
end
end

describeで何をテストしようとしているのかを示し、contextで条件を示して、その条件をWhenで実装する感じです。


GivenとWhenのタイミング

要はGivenは呼ばれた時に評価、WhenはThen実行直前に評価されると思っておくと良いと思います。

その意識で読みやすくするためにもGiven {...}は使わず、When {...}を使った方が良さそうです。

GivenはGiven(:result) {...}と、遅延評価を使えるように使いましょう。


よりきれいなWhenの使い方

理想は一つのcontextに一つのWhenがあるように書くことで、そうするとカバレッジも高くしやすいですし、なにより読みやすくなります。

なのでbefore(:each)も特別な理由がない限り使わないようにしています。


subjectと併用

rspec-givenのサンプルではGivenやWhenの結果をThenでテストしてたりしますがそうすると

「今なにについてテストを書いているのか」

が読む人にとっても書いてる人にとっても分かりにくくなるので普通にrspecのsubjectを使うと分かりやすいと思ってます。

特に普段からrspecでsubjectを使っていた場合は読みやすいかなと思います。

describe Stack do

def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end

# このstackについてテストを書いているように見えにくい
Given(:stack) { stack_with(initial_contents) }

context "with no items" do
Given(:initial_contents) { [] }
Then { stack.depth == 0 } # ん?stackってどこで定義したっけってなりやすい
end
end

下のようにsubjectを使う

describe Stack do

def stack_with(initial_contents)
stack = Stack.new
initial_contents.each do |item| stack.push(item) end
stack
end

# これがここでテストしたい対象だと分かるようにする
subject { stack_with(initial_contents) }

context "with no items" do
Given(:initial_contents) { [] }
Then { subject.depth == 0 } # subjectということはsubject句があるんだなと思いやすい
end
end

これはrspecへの慣れ具合にもよるかもしれません。


itは使わずにThenを使う

ThenはNatural Assertionが使えるitだと思って良いと思います。

Natural Assertionについてはrspec-givenのドキュメントを読むか私が以前書いたNatural Assertionについてを参照ください。

Thenはitと同じタイミングで実行されます。

なのでitは使わず、Thenを使うようにしています。


AndはThen内での評価を使いまわす

AndはThenに続けて使います。

例えばこのようになります

# userをDBに保存する場合

Given(:user) { User.create(name: name) }
Given(:name) { 'kakkunpakkun' }

Then { user.persisted? == true } # ここでuserが叩かれたときにDBに保存する
And { user.name == name } # ここでは新たに保存をせず、Thenで作られたuserを使う

Then { user.class == User } # ここでは新たにuserをDBに保存する

RSpec2まであったitsのように一つのインスタンスのattributesについてチェックしていきたい場合にムダな処理が走らなくて良いです。


公式のリポジトリはそれじゃない

ちょっと番外編

rspec-givenで検索するとこういう結果になりますが、それはrspec-givenの公式リポジトリじゃない!

以前はjimweirichさんの個人リポジトリが公式でしたが、今はrspec-given/rspec-givenがあります。

普通に検索するとだいぶメンテされてないライブラリに見えますが、公式はちゃんとアップデートされてるのでご安心を

https://github.com/rspec-given/rspec-given