Help us understand the problem. What is going on with this article?

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

はじめに

RSpecは難しい、よくわからない、といったコメントをときどき見かけます。
確かにちょっと独特な構文を持っていますし、機能も結構多いので「難しそう」と感じてしまう気持ちもわかります。
(構文については僕も最初見たときに「うげっ、なんか気持ちわるっ」と思った記憶がありますw)

しかし、RSpecに限らずどんなフレームワークでも同じですが、慣れてしまえばスラスラ書けますし、実際僕自身は「RSpecって便利だな-」と思いながらテストコードを書いています。

そこでこの記事では、僕が考える「最低限ここだけを押さえていれば大丈夫!!」なRSpecの構文や、僕が普段よく使う便利な機能をまとめてみます。

具体的には以下のような構文や機能です。

  • describe / it / expect の役割
  • ネストした describe
  • context の使い方
  • before の使い方
  • let / let! / subject の使い方
  • shared_examples の使い方
  • shared_context の使い方
  • pending と skip の使い分け

あと、できれば今後も次のようなRSpec関連の入門記事を書いていこうと思います。

対象となる読者

  • 「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を使用される方はこちらの動画も参考にしてみてください。
また、動画の作成中に一番外側のdescribeRSpec.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

describeRSpec.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 になることを期待する」テストになります。

ちなみに、 toeq の部分はマッチャと呼ばれる機能ですが、今回の記事ではあまり深入りしません。
とりあえず今は「期待値と実際の値が等しいことを確認してるんだなー」ということだけ理解してもらえれば大丈夫です。

参考: 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 でもう少し便利に

もうちょっと実践的な例を使って、 contextbefore の使い方を見ていきましょう。

ここではこんなクラスをテストします。

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 ブロックの中では変数のスコープが異なるためです。

ネストした describe や context の中で before を使う

beforedescribecontext ごとに用意することができます。
describecontext がネストしている場合は、親子関係に応じて 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 で置き換えるのもアリです。

userlet で置き換えてみましょう。

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 は必要になる瞬間まで呼び出されません。

上のコード例だと、こんな順番で呼び出されます。

  1. expect(user.greet).to が呼ばれる => user って何だ?
  2. let(:user) { User.new(params) } が呼ばれる => params って何だ?
  3. let(:params) { { name: 'たろう', age: age } } が呼ばれる => age って何だ?
  4. let(:age) { 12 } (または13) が呼ばれる
  5. 結果として expect(User.new(name: 'たろう', age: 12).greet).to を呼んだことになる

これを「before + インスタンス変数」で実現しようとすると結構面倒なことになると思います。
というか、僕はその書き方がぱっと思いつきませんでした。

そんなわけで、 let の遅延評価されるという特徴をうまく活かすと、効率の良いテストコードを書くことができます。

2015.5.11追記
letの利点をRSpecの開発者が説明している記事があったので翻訳してみました。
こちらも参考にしてみてください。

RSpecのletを使うのはどんなときか?(翻訳)

subject を使ってテスト対象のオブジェクトを1箇所にまとめる

テスト対象のオブジェクト(またはメソッドの実行結果)が明確に一つに決まっている場合は、 subject という機能を使ってテストコードをDRYにすることができます。

たとえば、先ほどのコード例ではどちらのexampleも user.greet の実行結果をテストしています。
そこで、 user.greetsubject に引き上げて、 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 = テストの主語」と解釈することもできそうです。

リファクタリングしてテストコードの完成!

ここまでは paramsuser を分けていましたが分けるメリットもなさそうなので、 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

これでいったんこのテストコードは完成です。

注意: 技巧的なテストコードは避けましょう

ここまでいろいろなテクニックを紹介していきましたが、無理に letsubject を多用する必要はありません。

完全にDRYなテストコードを目指してこういった機能を多用すると、過度に技巧的になってしまい、かえってテストコードが読みにくくなる恐れがあります。
なので、テストコードは「DRYさ」よりも「読みやすさ」を大事にしてください。
アプリケーション側のコードとは異なり、多少の重複は許容するようにしましょう。

これは、このあとに紹介する shared_examplesshared_context といったテクニックに関しても同様です。

it や context に渡す文字列は英語で書く?日本語で書く?

ここまでは日本人向けに日本語を多用してきましたが、RSpecは itcontext に渡す文字列を英語で書くと英語のテストドキュメント風に見えます。

たとえばこんな感じです。

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 の他に全く同じ役割の examplespecify があります。
これら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 になること' の方が若干自然かもしれませんが・・・)

RSpecの高度な機能

letsubject まで使いこなせれば十分初級レベルをクリアしていると思います。
ただし、それに加えてこういうテクニックも知っていると場合によっては役に立つかもしれません。

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_examplesit_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_examplesit_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_contextinclude_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_examplesshared_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 trueeq 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.firstnil を返してしまい、テストが失敗することになります。
nilblog を比較しようとした瞬間にレコードがデータベースに保存されます)

この問題を回避する方法のひとつは、 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させたいときは itxit に変更するとそのexampleは実行されなくなります。(pending 扱いになります)

RSpec.describe '何らかの理由で実行したくないクラス' do
  xit '実行したくないテスト' do
    expect(1 + 2).to eq 3

    expect(foo).to eq bar
  end
end

xit だけでなく、xspecifyxexample も同様にexample全体をskipします。

グループ全体をまとめてskipさせる: xdescribe / xcontext

it だけでなく、 describecontext にも x を付けることができます。

# グループ全体をskipする
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する
xcontext '管理者の場合' do
  it '社員情報を編集できる' do
    # ...
  end
  it '社員情報を削除できる' do
    # ...
  end
end

テストはあとで書く: 中身の無い it

it 'something' do ... enddo ... 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の公式ドキュメント

今回紹介した内容は、以下の公式ドキュメントを僕なりに要約したような内容になっています。
詳しい仕様を確認したい方は公式ドキュメントを読んでみてください。

RSpec Core 3.1

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テスト入門
Everyday Rails

本書の内容に関する詳しい情報はこちらのブログをどうぞ。

RSpec 3.1に完全対応!「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした