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

【RSepc】include_context, include_examples, it_behaves_like の違い

TL;DR(要約)

  • include_contextinclude_examples はエイリアスの関係。動作は変わらない。
  • include_(context|examples) は、現在のコンテキストに直接テストケースを埋め込む。
  • it_behaves_like は、新しいコンテキストを自動生成して、そこにテストケースを埋め込む。
  • 基本的に it_behaves_like を使った方が良い。

対象のバージョン

RSpec 3.7
RSpec 3.8
RSpec 3.9

はじめに

include_context, include_examples, it_behaves_like の違いと、良くあるハマりポイントについて解説します。
それぞれの基本的な使い方は、このページをご覧の方はご存じだと思うので省きます。
(知らない人はググってね)

include_context の動作

include_context は、 現在のコンテキストに直接テストケースを埋め込みます。

……え、意味が分からない? 大丈夫、私もドキュメントの "include the examples in the current context" という解説を初めて見たときは理解できませんでした。

言葉では説明しづらいので、コードで説明します。

例えば、以下のようにコードを記述したとき

# 共有したい example を定義
shared_examples 'test1' do
  it 'something が 1 であること' do
    expect(something).to eq 1
  end
end

# テストを記述
describe 'hoge' do
  let(:something) { 1 }

  include_context 'test1'
end

実行時には以下のように解釈されます。

describe 'hoge' do
  let(:something) { 1 }

  # include_context 'test1' の部分に直接埋め込まれる
  it 'something が 1 であること' do
    expect(something).to eq 1
  end
end

include_examples の動作

include_examples は、 include_context のエイリアス(別名)です。

つまり、 include_examples の動作は include_context と全く同じです。
どうして同じ機能が別々の名前で用意されているのかというと、 rspec は自然言語(人間が普段つかう言葉)に近い文章でテストが書けることを目指して作られているからです。
「人間が読んだ時に、より理解しやすい方を選んでね」って意図で、複数の名前が定義されているんですね。
rspec は他にも、違う名前で同じ振る舞いをするものが沢山あります。

it_behaves_like の動作

it_behaves_like は、 新しいコンテキストを自動生成して、そこにテストケースを埋め込みます。

これもコードで見た方が分かりやすいです。
以下のようにコードを記述すると、

# 共有したい example を定義
shared_examples 'test1' do
  it 'something が 1 であること' do
    expect(something).to eq 1
  end
end

# テストを記述
describe 'hoge' do
  let(:something) { 1 }

  it_behaves_like 'test1'
end

実行時には以下のように解釈されます。

describe 'hoge' do
  let(:something) { 1 }

  # it_behaves_like 'test1' の部分に、自動で新しい context が生成される
  context 'behaves like a test1' do
    it 'something が 1 であること' do
      expect(something).to eq 1
    end
  end
end

include_(context|examples) の罠

一見、直接埋め込もうが、自動でコンテキストを挿入しようが、たいした違いはなさそうに思えます。
しかし、実は include_(context|examples) には、「定義の上書き」と呼ばれる(というか僕が勝手にそう呼んでいる)罠があるのです。

以下のようなテストコードを考えてみましょう。

shared_examples 'test1' do
  let(:something) { 1 }

  it 'something が 1 であること' do
    expect(something).to eq 1
  end
end

shared_examples 'test2' do
  let(:something) { 2 }

  it 'something が 2 であること' do
    expect(something).to eq 2
  end
end

describe 'hoge' do
  include_context 'test1'
  include_context 'test2'
end

上記のテストは、実行時には以下のように解釈されます。

describe 'hoge' do
  let(:something) { 1 }

  it 'something が 1 であること' do
    expect(something).to eq 1
  end

  let(:something) { 2 }

  it 'something が 2 であること' do
    expect(something).to eq 2
  end
end

同じコンテキストに2つの let が埋め込まれることで、片方の定義が上書きされてしまい、「something が 1 であること」のテストが失敗します。
include_(context|example) をメソッド感覚で使うと、上記のようなミスを犯してしまいがちです。

一方、 it_behaves_like ならば、

describe 'hoge' do
  context 'behaves like a test1' do
    let(:something) { 1 }

    it 'something が 1 であること' do
      expect(something).to eq 1
    end
  end

  context 'behaves like a test2' do
    let(:something) { 2 }

    it 'something が 2 であること' do
      expect(something).to eq 2
    end
  end
end

のように context が分離されるため、安全です。

結局、どう使い分ければいいの?

include_(context|examples) には上述したような罠があるので、 常に it_behaves_like を使えばいいと思います。
「include_(context|examples) じゃないと困る」という場面はありません。(断言)

参考資料

https://relishapp.com/rspec/rspec-core/v/3-7/docs/example-groups/shared-examples
https://relishapp.com/rspec/rspec-core/v/3-8/docs/example-groups/shared-examples
https://relishapp.com/rspec/rspec-core/v/3-9/docs/example-groups/shared-examples

(補足説明)

"include_context と include_examples はエイリアスの関係" と書きましたが、実は alias include_context include_examples のように、はっきりエイリアス定義されているわけではないです。
ただ、例外時のメッセージ以外は全く同じ挙動をするよう実装されているので、実質、エイリアスと考えて問題ありません。
(2020-01-21 時点)

https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/example_group.rb

      def self.include_examples(name, *args, &block)
        find_and_eval_shared("examples", name, caller.first, *args, &block)
      end

      # (中略)

      def self.include_context(name, *args, &block)
        find_and_eval_shared("context", name, caller.first, *args, &block)
      end

      # (中略)

      def self.find_and_eval_shared(label, name, inclusion_location, *args, &customization_block)
        shared_module = RSpec.world.shared_example_group_registry.find(parent_groups, name)

        unless shared_module
          raise ArgumentError, "Could not find shared #{label} #{name.inspect}"
        end

        shared_module.include_in(
          self, Metadata.relative_path(inclusion_location),
          args, customization_block
        )
      end
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
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