Basics

(`describe`/`it`)

RSpec is a DSL for creating executable examples of how code is expected to
behave, organized in groups. It uses the words “describe” and “it” so we can
express concepts like a conversation:

"Describe an account when it is first opened."
"It has a balance of zero."

The describe method creates an example group. Within the block passed to
describe you can declare nested groups using the describe or context
methods, or you can declare examples using the it or specify methods.

Under the hood, an example group is a class in which the block passed to
describe or context is evaluated. The blocks passed to it are evaluated
in the context of an instance of that class.Scenarios

One group, one example

Givena file named “sample_spec.rb” with:

RSpec.describe "something" do
  it "does something" do
  end
end

WhenI run rspec sample_spec.rb -fdocThenthe output should contain:

something
  does something

Nested example groups (using `context`)

Givena file named “nested_example_groups_spec.rb” with:

RSpec.describe "something" do
  context "in one context" do
    it "does one thing" do
    end
  end

  context "in another context" do
    it "does another thing" do
    end
  end
end

WhenI run rspec nested_example_groups_spec.rb -fdocThenthe output should contain:

something
  in one context
    does one thing
  in another context
    does another thing

Shared examples

Shared examples let you describe behaviour of classes or modules. When declared,
a shared group’s content is stored. It is only realized in the context of
another example group, which provides any context the shared group needs to
run.

A shared group is included in another group using any of:

include_examples "name"      # include the examples in the current context
it_behaves_like "name"       # include the examples in a nested context
it_should_behave_like "name" # include the examples in a nested context
matching metadata            # include the examples in the current context

WARNING: Files containing shared groups must be loaded before the files that
use them. While there are conventions to handle this, RSpec does not do
anything special (like autoload). Doing so would require a strict naming
convention for files that would break existing suites.

WARNING: When you include parameterized examples in the current context multiple
times, you may override previous method definitions and last declaration wins.
So if you have this kind of shared example (or shared context)

RSpec.shared_examples "some example" do |parameter|
  \# Same behavior is triggered also with either `def something; 'some value'; end`
  \# or `define_method(:something) { 'some value' }`
  let(:something) { parameter }
  it "uses the given parameter" do
    expect(something).to eq(parameter)
  end
end

RSpec.describe SomeClass do
  include_examples "some example", "parameter1"
  include_examples "some example", "parameter2"
end

You’re actually doing this (notice that first example will fail):

RSpec.describe SomeClass do
  \# Reordered code for better understanding of what is happening
  let(:something) { "parameter1" }
  let(:something) { "parameter2" }

  it "uses the given parameter" do
    \# This example will fail because last let "wins"
    expect(something).to eq("parameter1")
  end

  it "uses the given parameter" do
    expect(something).to eq("parameter2")
  end
end

To prevent this kind of subtle error a warning is emitted if you declare multiple
methods with the same name in the same context. Should you get this warning
the simplest solution is to replace include_examples with it_behaves_like, in this
way method overriding is avoided because of the nested context created by it_behaves_like

Conventions:

  1. The simplest approach is to require files with shared examples explicitly
    from the files that use them. Keep in mind that RSpec adds the spec
    directory to the LOAD_PATH, so you can say require
    'shared_examples_for_widgets'
     to require a file at
    #{PROJECT_ROOT}/spec/shared_examples_for_widgets.rb.
  2. One convention is to put files containing shared examples in spec/support/
    and require files in that directory from spec/spec_helper.rb:Dir["./spec/support/**/*.rb"].sort.each { |f| require f } Historically, this was included in the generated spec/spec_helper.rb file in
    rspec-rails. However, in order to keep your test suite boot time down,
    it’s a good idea to not autorequire all files in a directory like this.
    When running only one spec file, loading unneeded dependencies or performing
    unneeded setup can have a significant, noticable effect on how long it takes
    before the first example runs.
  3. When all of the groups that include the shared group reside in the same file,
    just declare the shared group in that file.

Shared examples group included in two groups in one file

Givena file named “collection_spec.rb” with:

require "set"

RSpec.shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      expect(collection.size).to eq(3)
    end
  end

  describe "#include?" do
    context "with an item that is in the collection" do
      it "returns true" do
        expect(collection.include?(7)).to be(true)
      end
    end

    context "with an item that is not in the collection" do
      it "returns false" do
        expect(collection.include?(9)).to be(false)
      end
    end
  end
end

RSpec.describe Array do
  it_behaves_like "a collection"
end

RSpec.describe Set do
  it_behaves_like "a collection"
end

WhenI run rspec collection_spec.rb --format documentationThenthe examples should all passAndthe output should contain:

Array
  behaves like a collection
    initialized with 3 items
      says it has three items
    #include?
      with an item that is in the collection
        returns true
      with an item that is not in the collection
        returns false

Set
  behaves like a collection
    initialized with 3 items
      says it has three items
    #include?
      with an item that is in the collection
        returns true
      with an item that is not in the collection
        returns false

Providing context to a shared group using a block

Givena file named “shared_example_group_spec.rb” with:

require "set"

RSpec.shared_examples "a collection object" do
  describe "<<" do
    it "adds objects to the end of the collection" do
      collection << 1
      collection << 2
      expect(collection.to_a).to match_array([1, 2])
    end
  end
end

RSpec.describe Array do
  it_behaves_like "a collection object" do
    let(:collection) { Array.new }
  end
end

RSpec.describe Set do
  it_behaves_like "a collection object" do
    let(:collection) { Set.new }
  end
end

WhenI run rspec shared_example_group_spec.rb --format documentationThenthe examples should all passAndthe output should contain:

Array
  behaves like a collection object
    <<
      adds objects to the end of the collection

Set
  behaves like a collection object
    <<
      adds objects to the end of the collection

Passing parameters to a shared example group

Givena file named “shared_example_group_params_spec.rb” with:

RSpec.shared_examples "a measurable object" do |measurement, measurement_methods|
  measurement_methods.each do |measurement_method|
    it "should return #{measurement} from ##{measurement_method}" do
      expect(subject.send(measurement_method)).to eq(measurement)
    end
  end
end

RSpec.describe Array, "with 3 items" do
  subject { [1, 2, 3] }
  it_should_behave_like "a measurable object", 3, [:size, :length]
end

RSpec.describe String, "of 6 characters" do
  subject { "FooBar" }
  it_should_behave_like "a measurable object", 6, [:size, :length]
end

WhenI run rspec shared_example_group_params_spec.rb --format documentationThenthe examples should all passAndthe output should contain:

Array with 3 items
  it should behave like a measurable object
    should return 3 from #size
    should return 3 from #length

String of 6 characters
  it should behave like a measurable object
    should return 6 from #size
    should return 6 from #length

Aliasing `it_should_behave_like` to `it_has_behavior`

Givena file named “shared_example_group_spec.rb” with:

RSpec.configure do |c|
  c.alias_it_should_behave_like_to :it_has_behavior, 'has behavior:'
end

RSpec.shared_examples 'sortability' do
  it 'responds to <=>' do
    expect(sortable).to respond_to(:<=>)
  end
end

RSpec.describe String do
  it_has_behavior 'sortability' do
    let(:sortable) { 'sample string' }
  end
end

WhenI run rspec shared_example_group_spec.rb --format documentationThenthe examples should all passAndthe output should contain:

String
  has behavior: sortability
    responds to <=>

Sharing metadata automatically includes shared example groups

Givena file named “shared_example_metadata_spec.rb” with:

RSpec.shared_examples "shared stuff", :a => :b do
  it 'runs wherever the metadata is shared' do
  end
end

RSpec.describe String, :a => :b do
end

Shared examples are nestable by context

Consider the following spec

context_specific_examples_spec.rb

RSpec.describe "shared examples" do
  context "per context" do

    shared_examples "shared examples are nestable" do
      specify { expect(true).to eq true }
    end

    it_behaves_like "shared examples are nestable"
  end
end

Shared examples are accessible from offspring contexts

In this example, we show that it_behaves_like can be nested with a context

RSpec.describe "shared examples" do
  shared_examples "shared examples are nestable" do
    specify { expect(true).to eq true }
  end

  context "per context" do
    it_behaves_like "shared examples are nestable"
  end
end

Shared examples are isolated per context

In this example, we show that shared examples are isolated to only the context they are declared within.

RSpec.describe "shared examples" do
  context do
    shared_examples "shared examples are isolated" do
      specify { expect(true).to eq true }
    end
  end

  context do
    it_behaves_like "shared examples are isolated"
  end
end

In this case, we get an error:

Could not find shared examples \"shared examples are isolated\"





Shared context

Use shared_context to define a block that will be evaluated in the context of example groups either locally, using include_context in an example group, or globally using config.include_context.

When implicitly including shared contexts via matching metadata, the normal way is to define matching metadata on an example group, in which case the context is included in the entire group. However, you also have the option to include it in an individual example instead. RSpec treats every example as having a singleton example group (analogous to Ruby’s singleton classes) containing just the one example.

Consider the following Rspec example configuration

RSpec.configure do |rspec|
  # This config option will be enabled by default on RSpec 4,
  # but for reasons of backwards compatibility, you have to
  # set it on RSpec 3.
  #
  # It causes the host group and examples to inherit metadata
  # from the shared context.
  rspec.shared_context_metadata_behavior = :apply_to_host_groups
end

RSpec.shared_context "shared stuff", :shared_context => :metadata do
  before { @some_var = :some_value }
  def shared_method
    "it works"
  end
  let(:shared_let) { {'arbitrary' => 'object'} }
  subject do
    'this is the subject'
  end
end

RSpec.configure do |rspec|
  rspec.include_context "shared stuff", :include_shared => true
end

Declare a shared context and include it with `include_context`

Consider the following shared_context_example.rb

require "./shared_stuff.rb"

RSpec.describe "group that includes a shared context using 'include_context'" do
  include_context "shared stuff"

  it "has access to methods defined in shared context" do
    expect(shared_method).to eq("it works")
  end

  it "has access to methods defined with let in shared context" do
    expect(shared_let['arbitrary']).to eq('object')
  end

  it "runs the before hooks defined in the shared context" do
    expect(@some_var).to be(:some_value)
  end

  it "accesses the subject defined in the shared context" do
    expect(subject).to eq('this is the subject')
  end

  group = self

  it "inherits metadata from the included context" do |ex|
    expect(group.metadata).to include(:shared_context => :metadata)
    expect(ex.metadata).to include(:shared_context => :metadata)
  end
end

Declare a shared context, include it with `include_context` and extend it with an additional block

Givena file named “shared_context_example.rb” with:

require "./shared_stuff.rb"

RSpec.describe "including shared context using 'include_context' and a block" do
  include_context "shared stuff" do
    let(:shared_let) { {'in_a' => 'block'} }
  end

  it "evaluates the block in the shared context" do
    expect(shared_let['in_a']).to eq('block')
  end
end

rspec shared_context_example.rb

Declare a shared context and include it with metadata

Givena file named “shared_context_example.rb” with:

require "./shared_stuff.rb"

RSpec.describe "group that includes a shared context using metadata", :include_shared => true do
  it "has access to methods defined in shared context" do
    expect(shared_method).to eq("it works")
  end

  it "has access to methods defined with let in shared context" do
    expect(shared_let['arbitrary']).to eq('object')
  end

  it "runs the before hooks defined in the shared context" do
    expect(@some_var).to be(:some_value)
  end

  it "accesses the subject defined in the shared context" do
    expect(subject).to eq('this is the subject')
  end

  group = self

  it "inherits metadata from the included context" do |ex|
    expect(group.metadata).to include(:shared_context => :metadata)
    expect(ex.metadata).to include(:shared_context => :metadata)
  end
end

WhenI run rspec shared_context_example.rbThenthe examples should all pass

Declare a shared context and include it with metadata of an individual example

Givena file named “shared_context_example.rb” with:

require "./shared_stuff.rb"

RSpec.describe "group that does not include the shared context" do
  it "does not have access to shared methods normally" do
    expect(self).not_to respond_to(:shared_method)
  end

  it "has access to shared methods from examples with matching metadata", :include_shared => true do
    expect(shared_method).to eq("it works")
  end

  it "inherits metadata from the included context due to the matching metadata", :include_shared => true do |ex|
    expect(ex.metadata).to include(:shared_context => :metadata)
  end
end

WhenI run rspec shared_context_example.rbThenthe examples should all pass

Aliasing

describe and context are the default aliases for example_group. You can define your own aliases for example_group and give those custom aliases default metadata.

RSpec provides a few built-in aliases:

  • xdescribe and xcontext add :skip metadata to the example group in order to temporarily disable the examples.
  • fdescribe and fcontext add :focus metadata to the example group in order to make it easy to temporarily focus the example group (when combined with config.filter_run :focus.)

Scenarios

Custom example group aliases with metadata

Givena file named “nested_example_group_aliases_spec.rb” with:

RSpec.configure do |c|
  c.alias_example_group_to :detail, :detailed => true
end

RSpec.detail "a detail" do
  it "can do some less important stuff" do
  end
end

RSpec.describe "a thing" do
  describe "in broad strokes" do
    it "can do things" do
    end
  end

  detail "something less important" do
    it "can do an unimportant thing" do
    end
  end
end

WhenI run rspec nested_example_group_aliases_spec.rb --tag detailed -fdocThenthe output should contain:

a detail
  can do some less important stuff

a thing
  something less important