はじめに
RSpecは難しい、よくわからない、といったコメントをときどき見かけます。
確かにちょっと独特な構文を持っていますし、機能も結構多いので「難しそう」と感じてしまう気持ちもわかります。
(構文については僕も最初見たときに「うげっ、なんか気持ちわるっ」と思った記憶がありますw)
しかし、RSpecに限らずどんなフレームワークでも同じですが、慣れてしまえばスラスラ書けますし、実際僕自身は「RSpecって便利だな-」と思いながらテストコードを書いています。
そこでこの記事では、僕が考える「最低限ここだけを押さえていれば大丈夫!!」なRSpecの構文や、僕が普段よく使う便利な機能をまとめてみます。
具体的には以下のような構文や機能です。
- describe / it / expect の役割
- ネストした describe
- context の使い方
- before の使い方
- let / let! / subject の使い方
- shared_examples の使い方
- shared_context の使い方
- pending と skip の使い分け
あと、できれば今後も次のようなRSpec関連の入門記事を書いていこうと思います。
- よく使うマッチャあれこれ
- 公開しました! => 使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」
- モックの使い方
- 公開しました! => 使えるRSpec入門・その3「ゼロからわかるモックを使ったテストの書き方」
- よく使うCapybaraのDSL
対象となる読者
- 「RSpecってなんか怖そう」と思っているRSpec初心者の方
- 「何度か使ってみたけど、RSpecってようわからん」と思っているRSpec経験者の方
- Ruby関係のプロジェクトに放り込まれ、「RSpecでテストを書け」と言われて困惑している他のテストフレームワーク経験者の方
対象となるRSpecとRubyのバージョン
- RSpec 3.1.0
- Ruby 2.1.3
なお、今回の投稿ではRailsは出てきません。
「素のRubyプログラム」を対象にします。
RSpecのセットアップについて
RSpecのセットアップ方法については、 @yusabana さんが書かれた以下の記事を参考にしてください。
2019.7.22追記:セットアップ動画を作成しました
上のyusabanaさんの記事をベースにした、(Railsを使わない)RSpec環境の作成方法を動画にしました。
初めてRSpecを使用される方はこちらの動画も参考にしてみてください。
また、動画の作成中に一番外側のdescribe
はRSpec.describe
と書いた方が良いことに気づいたので、この記事(と、その2〜その4の記事)の内容もすべて修正してあります。
【2019年版】RailsじゃないRspec3環境を構築する方法 - YouTube
なお、この動画の実行環境は以下のとおりです。
- ruby 2.6.3
- rspec 3.8.0
- bundler 2.0.2
- rubygems 3.0.3
初めの一歩
それではさっそく始めましょう。
describe / it / expect の役割を理解する
一番単純なRSpecのテストはこんな記述になります。
RSpec.describe '四則演算' do
it '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
end
describe
(RSpec.describe
)はテストのグループ化を宣言します。
ここでは「四則演算に関するテストを書くよー」と宣言しています。
ちなみに describe は日本語にすると、「~を述べる」「~を説明する」「~を記述する」という意味です。
it
はテストを example という単位にまとめる役割をします。
it do ... end
の中のエクスペクテーション(期待値と実際の値の比較)がすべてパスすれば、その example はパスしたことになります。
expect(X).to eq Y
で記述するのがエクスペクテーションです。
expect には「期待する」という意味があるので、 expect(X).to eq Y
は「XがYに等しくなることを期待する」と読めます。
よって、 expect(1 + 1).to eq 2
は「1 + 1 が 2 になることを期待する」テストになります。
ちなみに、 to
と eq
の部分はマッチャと呼ばれる機能ですが、今回の記事ではあまり深入りしません。
とりあえず今は「期待値と実際の値が等しいことを確認してるんだなー」ということだけ理解してもらえれば大丈夫です。
参考: should が expect に変更された理由について
RSpec 2.10以前は should
を使って次のようなエクスペクテーションを書いていました。
(1 + 2).should eq 3
# または
(1 + 2).should == 3
should
ではなく、 expect
が使われるようになった理由は、 should
だと稀に不具合が発生することがわかったからです。
should
はメタプログラミング(いわゆる黒魔術)を使っていたので、問題が発生したんですね。
詳しい理由はこちらに載っています。
RSpec's New Expectation Syntax (英語)
複数の example
describe
の中には複数の example(it do ... end
)が書けます。
RSpec.describe '四則演算' do
it '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
it '10 - 1 は 9 になること' do
expect(10 - 1).to eq 9
end
end
テストを実行すると、以下のような結果が表示されます。
2 examples, 0 failures, 2 passed
Finished in 0.002592 seconds
Process finished with exit code 0
上の例では it do ... end
が2回登場しているので、「2つの example がパスした」と出力されています。
複数のエクスペクテーション
1つの example の中に複数のエクスペクテーションを書くのもOKです。
RSpec.describe '四則演算' do
it '全部できること' do
expect(1 + 2).to eq 3
expect(10 - 1).to eq 9
expect(4 * 8).to eq 32
expect(40 / 5).to eq 8
end
end
ただし、こうしてしまうと途中でテストが失敗したときに、その先のエクスペクテーションがパスするのかしないのかが予想できません。
なので、原則として「1つの example につき1つのエクスペクテーション」で書いた方がテストの保守性が良くなります。(もちろん、原則なので必要であれば破っても構いません)
ネストした describe
describe
はいくつでも書けますし、ネストさせることもできます。
基本的な役割は「テストのグループ化」なので、次のようにグループ化することもできます。
RSpec.describe '四則演算' do
describe '足し算' do
it '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
end
describe '引き算' do
it '10 - 1 は 9 になること' do
expect(10 - 1).to eq 9
end
end
end
適切にグループ化すると、「この describe ブロックはこの機能をテストしてるんだな」と読み手がテストコードを理解しやすくなります。
なお、一番外側のdescribe
以外はRSpec.
を省略できます。
context と before でもう少し便利に
もうちょっと実践的な例を使って、 context
と before
の使い方を見ていきましょう。
ここではこんなクラスをテストします。
class User
def initialize(name:, age:)
@name = name
@age = age
end
def greet
if @age <= 12
"ぼくは#{@name}だよ。"
else
"僕は#{@name}です。"
end
end
end
「初めの一歩」で学んだ知識を使うと次のようなテストが書けます。
RSpec.describe User do
describe '#greet' do
it '12歳以下の場合、ひらがなで答えること' do
user = User.new(name: 'たろう', age: 12)
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
it '13歳以上の場合、漢字で答えること' do
user = User.new(name: 'たろう', age: 13)
expect(user.greet).to eq '僕はたろうです。'
end
end
end
ちなみにgreet は日本語で「あいさつする」の意味です。
このままでも問題ないといえば問題ないのですが、もう少しRSpecらしくテストをリファクタリングしてみましょう。
参考: 「初めの一歩」とちょっと違うところ
describe
には describe User
のように、文字列ではなくクラスを渡すこともできます。
また、「インスタンスメソッドの greet メソッドをテストしますよ」という意味で describe '#greet'
のように書くこともよくあります。
context で条件別にグループ化する
RSpecには describe
以外にも context
という機能でテストをグループ化することもできます。
どちらも機能的には同じですが、 context
は条件を分けたりするときに使うことが多いです。
ちなみに、contextは日本語で「文脈」や「状況」の意味になります。
ここでは「12歳以下の場合」と「13歳以上の場合」という二つの条件にグループ分けしてみました。
RSpec.describe User do
describe '#greet' do
context '12歳以下の場合' do
it 'ひらがなで答えること' do
user = User.new(name: 'たろう', age: 12)
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
it '漢字で答えること' do
user = User.new(name: 'たろう', age: 13)
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
describe
と同様、 context
で適切にグループ化すると、「この context ブロックはこういう条件の場合をテストしてるんだな」と読み手がテストコードを理解しやすくなります。
before で共通の前準備をする
before do ... end
で囲まれた部分は example の実行前に毎回呼ばれます。
before
ブロックの中では、テストを実行する前の共通処理やデータのセットアップ等を行うことが多いです。
少々強引ですが、サンプルコードでは name: 'たろう'
が重複しているので、DRYにしてみましょう。
RSpec.describe User do
describe '#greet' do
before do
@params = { name: 'たろう' }
end
context '12歳以下の場合' do
it 'ひらがなで答えること' do
user = User.new(**@params.merge(age: 12))
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
it '漢字で答えること' do
user = User.new(**@params.merge(age: 13))
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
上の例を見るとわかるように、 ローカル変数ではなく、インスタンス変数にデータをセットしています。
これは before
ブロックと it
ブロックの中では変数のスコープが異なるためです。
(@params
の前に付いている**
はハッシュオブジェクトをキーワード引数に変換するために必要なdouble splat演算子です)
ネストした describe や context の中で before を使う
before
は describe
や context
ごとに用意することができます。
describe
や context
がネストしている場合は、親子関係に応じて before
が順番に呼ばれます。
先ほどのコード例を次のように変えてみましょう。
RSpec.describe User do
describe '#greet' do
before do
@params = { name: 'たろう' }
end
context '12歳以下の場合' do
before do
@params.merge!(age: 12)
end
it 'ひらがなで答えること' do
user = User.new(**@params)
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
before do
@params.merge!(age: 13)
end
it '漢字で答えること' do
user = User.new(**@params)
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
念のため、 before
がそれぞれどのように呼ばれるのか確認しておいてください。
上の例の場合、 @params = { name: 'たろう' }
の部分は「12歳以下の場合」でも「13歳以上の場合」でも呼ばれます。
一方、 @params.merge!(age: 12)
と @params.merge!(age: 13)
はそれぞれ「12歳以下の場合」と「13以上の場合」の中でしか呼ばれません。
呼ばれる順番はもちろん、親、子の順です。
応用(ちょっと高度なテクニック)
ここまでに学んだ内容を使えば、あなたも「ふつうにRSpecでテストを書ける」ようになるはずです。
しかし、あなたの周りの「RSpec熟練者」はこんなテクニックを使っているかもしれません。
念のため、ここから先の内容も知っておきましょう。
インスタンス変数のかわりに let を使う
先ほど出てきたコード例では、インスタンス変数の @params
を使っていました。
しかし、RSpecではこのインスタンス変数を let
という機能で置き換えることができます。
実際に let
で置き換えたコードを見てみましょう。
RSpec.describe User do
describe '#greet' do
let(:params) { { name: 'たろう' } }
context '12歳以下の場合' do
before do
params.merge!(age: 12)
end
it 'ひらがなで答えること' do
user = User.new(**params)
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
before do
params.merge!(age: 13)
end
it '漢字で答えること' do
user = User.new(**params)
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
let(:foo) { ... }
のように書くと、 { ... }
の中の値が foo
として参照できる、というのが let
の基本的な使い方です。
ただ、上の例では { { name: 'たろう' } }
と、 { }
が2回出てくるのでややこしくなってます。
外側の { }
はRubyのブロックで、 内側の { }
はハッシュリテラルです。
わかりづらい方は、こんなふうに書くとイメージが付きやすいかもしれません。
# let(:params) { { name: 'たろう' } } と同じ意味のコード
let(:params) do
hash = {}
hash[:name] = 'たろう'
hash
end
ところで、みなさんの中には「インスタンス変数のかわりに let
を使うのって何かメリットがあるの?」と思った人がいるかもしれません。
let
を使うメリットはこのあとで説明します。
user を let にする
インスタンス変数だけでなく、ローカル変数を let
で置き換えるのもアリです。
user
も let
で置き換えてみましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう' } }
context '12歳以下の場合' do
before do
params.merge!(age: 12)
end
it 'ひらがなで答えること' do
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
before do
params.merge!(age: 13)
end
it '漢字で答えること' do
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
重複していた user = User.new(**params)
の部分を共通化することができました。
let のメリットを活かして age も let で置き換える
上のコードでは before
ブロックの中で params.merge!(age: 12)
みたいなコードを書いているのがあまりクールじゃありません。
どうせなら、ここも let
で置き換えてスッキリさせましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう', age: age } }
context '12歳以下の場合' do
let(:age) { 12 }
it 'ひらがなで答えること' do
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
let(:age) { 13 }
it '漢字で答えること' do
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
さて、ここで先ほどちらっと触れた「let
のメリット」が現れています。
let
は「before
+ インスタンス変数」を使うときとは異なり、 遅延評価される という特徴があります。
すなわち、 let
は必要になる瞬間まで呼び出されません。
上のコード例だと、こんな順番で呼び出されます。
-
expect(user.greet).to
が呼ばれる =>user
って何だ? -
let(:user) { User.new(**params) }
が呼ばれる =>params
って何だ? -
let(:params) { { name: 'たろう', age: age } }
が呼ばれる =>age
って何だ? -
let(:age) { 12 }
(または13) が呼ばれる - 結果として
expect(User.new(name: 'たろう', age: 12).greet).to
を呼んだことになる
これを「before
+ インスタンス変数」で実現しようとすると結構面倒なことになると思います。
というか、僕はその書き方がぱっと思いつきませんでした。
そんなわけで、 let
の遅延評価されるという特徴をうまく活かすと、効率の良いテストコードを書くことができます。
2015.5.11追記
letの利点をRSpecの開発者が説明している記事があったので翻訳してみました。
こちらも参考にしてみてください。
subject を使ってテスト対象のオブジェクトを1箇所にまとめる
テスト対象のオブジェクト(またはメソッドの実行結果)が明確に一つに決まっている場合は、 subject
という機能を使ってテストコードをDRYにすることができます。
たとえば、先ほどのコード例ではどちらのexampleも user.greet
の実行結果をテストしています。
そこで、 user.greet
を subject
に引き上げて、 example の中から消してみましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう', age: age } }
subject { user.greet }
context '12歳以下の場合' do
let(:age) { 12 }
it 'ひらがなで答えること' do
is_expected.to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
let(:age) { 13 }
it '漢字で答えること' do
is_expected.to eq '僕はたろうです。'
end
end
end
end
subject { user.greet }
を宣言したので、今まで expect(user.greet).to eq 'ぼくはたろうだよ。'
と書いていた部分が is_expected.to eq 'ぼくはたろうだよ。'
に変わりました。
さらに、 it
に渡す文字列('ひらがなで答えること' など)を省略してみます。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう', age: age } }
subject { user.greet }
context '12歳以下の場合' do
let(:age) { 12 }
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '13歳以上の場合' do
let(:age) { 13 }
it { is_expected.to eq '僕はたろうです。' }
end
end
end
it { is_expected.to eq 'ぼくはたろうだよ。' }
は "it is expected to eq 'ぼくはたろうだよ。'" と、自然な英文っぽく読むことができます。
ちなみに subject は日本語で「主語」や「対象」という意味があります。
こんな書き方はできませんが、 user.greet { is_expected.to eq 'ぼくはたろうだよ。' }
と考えると、 「subject = テストの主語」と解釈することもできそうです。
リファクタリングしてテストコードの完成!
ここまでは params
と user
を分けていましたが分けるメリットもなさそうなので、 params
の内容をインライン化してしまいましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(name: 'たろう', age: age) }
subject { user.greet }
context '12歳以下の場合' do
let(:age) { 12 }
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '13歳以上の場合' do
let(:age) { 13 }
it { is_expected.to eq '僕はたろうです。' }
end
end
end
これでいったんこのテストコードは完成です。
(User.new
の引数はハッシュオブジェクトではなく、純粋なキーワード引数に変わったため、**
も不要になっています)
注意: 技巧的なテストコードは避けましょう
ここまでいろいろなテクニックを紹介していきましたが、無理に let
や subject
を多用する必要はありません。
完全にDRYなテストコードを目指してこういった機能を多用すると、過度に技巧的になってしまい、かえってテストコードが読みにくくなる恐れがあります。
なので、テストコードは「DRYさ」よりも「読みやすさ」を大事にしてください。
アプリケーション側のコードとは異なり、多少の重複は許容するようにしましょう。
これは、このあとに紹介する shared_examples
や shared_context
といったテクニックに関しても同様です。
it や context に渡す文字列は英語で書く?日本語で書く?
ここまでは日本人向けに日本語を多用してきましたが、RSpecは it
や context
に渡す文字列を英語で書くと英語のテストドキュメント風に見えます。
たとえばこんな感じです。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう', age: age } }
context 'when 12 years old or younger' do
let(:age) { 12 }
it 'greets in hiragana' do
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context 'when 13 years old or older' do
let(:age) { 13 }
it 'greets in kanji' do
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
context
には 'when ~(~であるとき)' や 'with ~(~がある場合)' 、 'without ~(~がない場合)' といった説明の文字列を渡すと、条件でグループ分けしていることが明確になります。
また、 it
のうしろにはテストしようとするメソッドの「振る舞い」を表す動詞をつなげて、仕様を英文化してください。
・・・ということができれば理想的なのですが、実際問題、思った通りの英文をさっと書き上げるのはなかなか難しいと思います。
なので、日本人しか読まないテストコードなのであれば、日本語で書いても問題ないと僕は考えています。
しっくり合う英文を考えるのに時間を食うぐらいなら、その時間でテストコードを書いた方が時間の有効利用ができますからね。
それに、がんばって英文を書いても文法がめちゃくちゃで他の人が理解できない、なんていうリスクも出てくるかも・・・。
というわけで、英語が苦手な人は無理せず日本語で書いちゃいましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(**params) }
let(:params) { { name: 'たろう', age: age } }
context '12歳以下の場合' do
let(:age) { 12 }
it 'ひらがなで答えること' do
expect(user.greet).to eq 'ぼくはたろうだよ。'
end
end
context '13歳以上の場合' do
let(:age) { 13 }
it '漢字で答えること' do
expect(user.greet).to eq '僕はたろうです。'
end
end
end
end
it のエイリアス = example と specify
ここまでは it 'xxx'
の形式でテストを書いてきましたが、 RSpecには it
の他に全く同じ役割の example
と specify
があります。
これら3つのメソッドはエイリアスの関係にあるので、以下のテストコードは内部的には全く同じ意味になります。
it '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
specify '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
example '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
なぜ3種類も同じメソッドがあるのかというと、自然な英文を作るためです。
# それ(it)はユーザー名を返す
it 'returns user name' do
# ...
end
# 会社は社員を持つことを仕様として明記(specify)する
specify 'Company has employees' do
# ...
end
# fizz_buzzメソッドの実行例(example)
example '#fizz_buzz' do
# ...
end
ただしこの記事では英語ではなく日本語を使っていくので、特に使い分けずに毎回 it
を使っています。
(まあ it '1 + 1 は 2 になること'
よりも specify '1 + 1 は 2 になること'
や example '1 + 1 は 2 になること'
の方が若干自然かもしれませんが・・・)
2022.2.9追記
it
/example
/specify
の使い分けについては、以下の記事でさらに詳しく議論しています。
RSpecの高度な機能
let
や subject
まで使いこなせれば十分初級レベルをクリアしていると思います。
ただし、それに加えてこういうテクニックも知っていると場合によっては役に立つかもしれません。
example の再利用: shared_examples と it_behaves_like
上で作ったテストコードに、もうちょっとテストパターンを増やしてみましょう。
12歳と13歳だけでなく、もっと小さい子ども(0歳)や、もっと大きな大人(100歳)にもあいさつしてもらいます。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(name: 'たろう', age: age) }
subject { user.greet }
context '0歳の場合' do
let(:age) { 0 }
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '12歳の場合' do
let(:age) { 12 }
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '13歳の場合' do
let(:age) { 13 }
it { is_expected.to eq '僕はたろうです。' }
end
context '100歳の場合' do
let(:age) { 100 }
it { is_expected.to eq '僕はたろうです。' }
end
end
end
見てもらえばわかると思いますが、同じexampleが2回ずつ登場しています。
こういう場合は、 shared_examples
と it_behaves_like
という機能を使うと、example を再利用することができます。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(name: 'たろう', age: age) }
subject { user.greet }
shared_examples '子どものあいさつ' do
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '0歳の場合' do
let(:age) { 0 }
it_behaves_like '子どものあいさつ'
end
context '12歳の場合' do
let(:age) { 12 }
it_behaves_like '子どものあいさつ'
end
shared_examples '大人のあいさつ' do
it { is_expected.to eq '僕はたろうです。' }
end
context '13歳の場合' do
let(:age) { 13 }
it_behaves_like '大人のあいさつ'
end
context '100歳の場合' do
let(:age) { 100 }
it_behaves_like '大人のあいさつ'
end
end
end
shared_examples 'foo' do ... end
で再利用したいexampleを定義し、 it_behaves_like 'foo'
で定義したexampleを呼び出すイメージです。
ちなみに shared_examples
と it_behaves_like
は日本語にするとそれぞれ、「共有されているexample」と「~のように振る舞うこと」というふうに訳せます。
context の再利用: shared_context と include_context
User
クラスに新しいメソッド、 child?
を追加してみます。
class User
def initialize(name:, age:)
@name = name
@age = age
end
def greet
if child?
"ぼくは#{@name}だよ。"
else
"僕は#{@name}です。"
end
end
def child?
@age <= 12
end
end
せっかくなので greet
メソッドだけでなく、 child?
メソッドもテストしておきましょう。
RSpec.describe User do
describe '#greet' do
let(:user) { User.new(name: 'たろう', age: age) }
subject { user.greet }
context '12歳以下の場合' do
let(:age) { 12 }
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '13歳以上の場合' do
let(:age) { 13 }
it { is_expected.to eq '僕はたろうです。' }
end
end
describe '#child?' do
let(:user) { User.new(name: 'たろう', age: age) }
subject { user.child? }
context '12歳以下の場合' do
let(:age) { 12 }
it { is_expected.to eq true }
end
context '13歳以上の場合' do
let(:age) { 13 }
it { is_expected.to eq false }
end
end
end
テストコードを書いてみると、どちらのテストも「12歳以下の場合」と「13歳以上の場合」で、同じ context が登場しています。
こういう場合は shared_context
と include_context
を使うと、 context を再利用することができます。
RSpec.describe User do
let(:user) { User.new(name: 'たろう', age: age) }
shared_context '12歳の場合' do
let(:age) { 12 }
end
shared_context '13歳の場合' do
let(:age) { 13 }
end
describe '#greet' do
subject { user.greet }
context '12歳以下の場合' do
include_context '12歳の場合'
it { is_expected.to eq 'ぼくはたろうだよ。' }
end
context '13歳以上の場合' do
include_context '13歳の場合'
it { is_expected.to eq '僕はたろうです。' }
end
end
describe '#child?' do
subject { user.child? }
context '12歳以下の場合' do
include_context '12歳の場合'
it { is_expected.to eq true }
end
context '13歳以上の場合' do
include_context '13歳の場合'
it { is_expected.to eq false }
end
end
end
shared_examples
のときと同じように、 shared_context 'foo' do ... end
で再利用したい context を定義し、 include_context 'foo'
で定義した context を呼び出すイメージです。
注意: テストコードはDRYさよりも読みやすさを重視しましょう
繰り返しになりますが、 shared_examples
も shared_context
も多用しすぎると他の人が読んだり、あとで自分が読み直したりしたときに読み手を混乱させる恐れがあります。
テストコードの場合はDRYさよりも読みやすさを重視して、多少の重複は許容する方が良い結果に結びつくはずです。
参考: be_truthy / be_falsey という書き方もあります
it { is_expected.to eq true }
と it { is_expected.to eq false }
はそれぞれ it { is_expected.to be_truthy }
と it { is_expected.to be_falsey }
と書き直すことができます。
この話題はマッチャに関することなので、この記事ではあまり深追いせずに eq true
と eq false
を使っています。
遅延評価される let と事前に実行される let!
let
は使いこなすとなかなか便利な機能なのですが、「遅延評価される」という特徴がアダになってわかりづらいテストの失敗を招くことがあります。
たとえば、Railsで次のような Blog
モデルのテストを書くとテストが失敗します。
(すいません、ここだけRailsのテストを使わせてください)
RSpec.describe Blog do
let(:blog) { Blog.create(title: 'RSpec必勝法', content: 'あとで書く') }
it 'ブログの取得ができること' do
expect(Blog.first).to eq blog
end
end
このテストが失敗する理由がわかるでしょうか?
expect(Blog.first).to eq blog
の部分に注目してください。
Blog.first
を呼び出した時点ではまだ let(:blog)
が実行されていないため、レコードがデータベースに保存されていません。
そのため、 Blog.first
が nil
を返してしまい、テストが失敗することになります。
(nil
と blog
を比較しようとした瞬間にレコードがデータベースに保存されます)
この問題を回避する方法のひとつは、 before
ブロックの中で明示的に let(:blog)
を呼び出すことです。
明示的に呼び出すことで、 example の実行前にレコードをデータベースに保存することができます。
RSpec.describe Blog do
let(:blog) { Blog.create(title: 'RSpec必勝法', content: 'あとで書く') }
before do
blog # ここでデータベースにレコードを保存する
end
it 'ブログの取得ができること' do
expect(Blog.first).to eq blog
end
end
しかし、こんな書き方をしなくてもこれと同じことをしてくれる let!
があります。
let!
を使うと example の実行前に let!
で定義した値が作られるようになります。
RSpec.describe Blog do
let!(:blog) { Blog.create(title: 'RSpec必勝法', content: 'あとで書く') }
it 'ブログの取得ができること' do
expect(Blog.first).to eq blog
end
end
というわけで、 let
の遅延評価がテストの失敗の原因になっている場合は、かわりに let!
を使うと便利です。
どうしても動かないテストに保留マークを付ける: pending
本来はパスすべきなのに、なぜかどうしてもテストがパスしない・・・こんなときは pending
で保留マークを付けておきましょう。
RSpec.describe '繊細なクラス' do
it '繊細なテスト' do
expect(1 + 2).to eq 3
pending 'この先はなぜかテストが失敗するのであとで直す'
# パスしないエクスペクテーション(実行される)
expect(foo).to eq bar
end
end
pending
が変わっているのは、そこで実行を中断するのではなく、そのまま実行を続けることです。
そして、 テストが失敗すれば 成功でも失敗でもない「pending(保留)」としてマークされます。
もし最後までテストがパスした場合は pending にならず、「なんでパスすんねん!!」とRSpecに怒られてテストが失敗します。
ちょっとひねくれた仕様ですが、「いつのまにか直ってた」という怪奇現象を避けることができるので、意外と便利な仕様かもしれません。
問答無用でテストの実行を止める: skip
一方、本当にそこでテストの実行を止めたいという場合は skip
を使います。
RSpec.describe '何らかの理由で実行したくないクラス' do
it '実行したくないテスト' do
expect(1 + 2).to eq 3
skip 'とりあえずここで実行を保留'
# ここから先は実行されない
expect(foo).to eq bar
end
end
pending
とは異なり、 skip
はそこから先は実行せずにテストを pending としてマークします。
とはいえ、個人的には skip
を使うユースケースがあまり思い浮かびません。
登場頻度は pending
よりもずっと少ないのではないかと思います。
手っ取り早くexample全体をskipさせる: xit
example全体を手っ取り早くskipさせたいときは it
を xit
に変更するとそのexampleは実行されなくなります。(pending 扱いになります)
RSpec.describe '何らかの理由で実行したくないクラス' do
xit '実行したくないテスト' do
expect(1 + 2).to eq 3
expect(foo).to eq bar
end
end
xit
だけでなく、xspecify
や xexample
も同様にexample全体をskipします。
グループ全体をまとめてskipさせる: xdescribe / xcontext
it
だけでなく、 describe
や context
にも x
を付けることができます。
# グループ全体をskipする
RSpec.xdescribe '四則演算' do
it '1 + 1 は 2 になること' do
expect(1 + 1).to eq 2
end
it '10 - 1 は 9 になること' do
expect(10 - 1).to eq 9
end
end
# グループ全体をskipする
RSpec.xcontext '管理者の場合' do
it '社員情報を編集できる' do
# ...
end
it '社員情報を削除できる' do
# ...
end
end
テストはあとで書く: 中身の無い it
it 'something' do ... end
の do ... end
を省略すると、その場合も pending のテストとしてマークされます。
これはメソッドを実装するときに、テストパターンを考えながらそのままドキュメント化(RSpec化)できるので結構便利です。
たとえば、「User
クラスに good_bye
メソッドを追加したいなあ」と思った場合は、実装に移る前にこんな感じでRSpec上で仕様を設計することができます(実装前のToDoリストと見なすのも良いかもしれません)。
RSpec.describe User do
describe '#good_bye' do
context '12歳以下の場合' do
it 'ひらがなでさよならすること'
end
context '13歳以上の場合' do
it '漢字でさよならすること'
end
end
end
pending のテストがなくなったときが good_bye
メソッドの実装が完了したときです。
参考: これってTDD?
実装の前にテストを書くからTDD(テスト駆動開発)っぽいですが、厳密にいうと違うと思います。
まず、これは pending なので、レッド(失敗)にはなりません(色でいうとイエローですね)。
あと、TDDでは実装よりもテストコード(テストの中身)を先に書く必要がありますが、ここではどちらを先に書くか特に決めていません。
なので、TDD本来のプロセスとはちょっと異なります。
ちなみに僕の場合、「TDDは使いたいときだけ使う」主義です。
実務でコードを書く場合は以下の3パターンのどれかになります。
- 先に実装してあとでテストを書く
- 先にテストを書いてから実装する(TDD)
- 実装後に手作業と目視で確認して、テストコードは書かない
TDD原理主義(何が何でもTDD)を採用するとかえって開発効率が悪くなる場合も多々あるので、費用対効果が一番高そうな方法をその都度選択している感じです。
まとめ
というわけで、今回の記事ではRSpecの基本的な構文やよく使う便利機能を紹介してみました。
初心者向けの内容から上級者向けの内容まで一気に説明したので、読んでいたみなさんも大変だったかもしれません。
しかし、これぐらいの内容を押さえておけばRSpecの上級者が書いたコードもだいたい理解できるようになるんじゃないかと思います。
RSpecにはたくさんの機能がありますし、ぱっと見、複雑で奇妙に見える構文も多いです。
しかし、適材適所で(ちょっと奇妙だけど)便利な機能を使い、過度に技巧的なテストコードを避けるようにすれば、「RSpecのおいしいところ」を活かした「読みやすくて保守しやすいテストコード」ができあがるはずです。
これからRSpecをさわる人も、何度か使ったことがある人も、今回の記事を参考にして「RSpecのおいしいところ」を活用できるようになってもらえると嬉しいです。
さて、時間があれば同じような今後もRSpec入門記事として、以下のようなテーマを取り上げたいと考えています。
- よく使うマッチャあれこれ
- 公開しました! => 使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」
- モックの使い方
- 公開しました! => 使えるRSpec入門・その3「ゼロからわかるモックを使ったテストの書き方」
- よく使うCapybaraのDSL
「続きが読みたい!」という方はこの記事をストックしてもらうと、新しい記事を投稿したときに通知メールを送りますので、どうぞよろしくお願いします!
あわせて読みたい
RSpecの公式ドキュメント
今回紹介した内容は、以下の公式ドキュメントを僕なりに要約したような内容になっています。
詳しい仕様を確認したい方は公式ドキュメントを読んでみてください。
TDDのプロセスを重視したRSpecの入門記事
また、RSpec関連の入門記事としてはこちらの記事もオススメです。
「RSpecの入門とその一歩先へ ~RSpec 3バージョン~」
この記事の裏話
今回の投稿を書いた動機や、書く時に意識したことをブログにまとめてみました。
「使えるRSpec入門・その1 RSpecの基本的な構文や便利な機能を理解する」を書いた動機や意識したこととか
PR: RSpec 3.1に対応した「Everyday Rails - RSpecによるRailsテスト入門」が発売中です
僕が翻訳者として携わった 「Everyday Rails - RSpecによるRailsテスト入門」 という電子書籍が発売中です。
Railsアプリケーションを開発している方で、実践的なテストの書き方を学んでみたいという人には最適な一冊です。
現行バージョンはRSpec 3.1に対応しています。
一度購入すれば将来無料でアップデート版をダウンロードできるのも本書の特徴の一つです。
よかったらぜひ読んでみてください!
Everyday Rails - RSpecによるRailsテスト入門
本書の内容に関する詳しい情報はこちらのブログをどうぞ。
RSpec 3.1に完全対応!「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました