In RSpec, when should a spec use it_behaves_like rather than include_examples, and how does shared_context differ from shared_examples?
answer
- nested group versus current group
- parameters and a customization block
- last let definition wins
- shared_context aliases shared_examples
- include_context for setup only
basics
~20 sit_behaves_like evaluates shared examples in a new nested group, isolating their lets; include_examples evaluates them in the current group, where a second inclusion overrides the first one's lets. shared_context is an alias used for setup via include_context.
solid answer
~40 s`RSpec.shared_examples "a collection" do |item| ... end` stores a block; it only runs when a group includes it. `it_behaves_like "a collection", "milk"` creates a nested group described as "behaves like a collection" and evaluates the block there, so each inclusion has its own `let`s. `include_examples` evaluates the block directly in the current group: including the same parameterized examples twice defines the same `let` twice, and the last definition wins for every example, so the first set asserts against the wrong value. `shared_context` is an alias of `shared_examples`; the difference is intent: it holds `let`s, hooks and helpers, and `include_context` evaluates it in the current group so the examples there can use them. Shared groups must be loaded before the file that uses them, and a missing name raises `ArgumentError`.
code
ruby · 12 linesRSpec.shared_context "with a stocked list" do
let(:list) { ShoppingList.new }
before { list.add("milk") }
end
RSpec.describe ShoppingList do
include_context "with a stocked list"
it "reports its size" do
expect(list.items.size).to eq(1)
end
endgo deeper
Recall that shared_examples defines reusable examples and it_behaves_like pulls them into a group.
Explain nested versus current-group evaluation and why include_context is the natural partner of shared_context.
Catch the last-let-wins bug from repeated include_examples, and push back on shared examples that hide undocumented host dependencies.
Decide when a behavioural contract across many classes justifies shared examples, weighing reuse against specs a reader can follow in one file.
## Stored blocks, evaluated on inclusion A **shared group** is a named block that RSpec stores when it is defined and evaluates later, inside whichever example group includes it. Nothing in it runs at definition time. ```ruby RSpec.shared_examples "a list that deduplicates" do |item| let(:entry) { item } it "keeps one #{item}" do 2.times { list.add(entry) } expect(list.items.count(entry)).to eq(1) end end RSpec.describe ShoppingList do let(:list) { ShoppingList.new } it_behaves_like "a list that deduplicates", "milk" it_behaves_like "a list that deduplicates", "eggs" end ``` The shared block reads `list` from the including group: the host provides the context, the shared group provides the behaviour. ## The ways to include one | Call | Where the block is evaluated | Typical content | |---|---|---| | `it_behaves_like name, *args` | a **new nested group**, described "behaves like name" | examples | | `it_should_behave_like name, *args` | a new nested group, described "it should behave like name" | examples | | `include_examples name, *args` | the **current** group | examples | | `include_context name, *args` | the **current** group | `let`, hooks, helper methods | All four accept extra arguments, passed to the block's parameters, and an optional block that is evaluated in the target group after the shared content, which lets the caller override a `let` for that inclusion only. ## Why include_examples twice goes wrong Because `include_examples` evaluates the block in the current group, every `let` it defines becomes a method **of that group**. Including the same parameterized examples twice defines `let(:entry)` twice in one class: 1. The first inclusion defines `entry` as "milk" and adds an example titled "keeps one milk". 2. The second defines `entry` again as "eggs" and adds "keeps one eggs". 3. Methods on one class have one definition, so the second wins; the example titled "keeps one milk" now actually tests eggs. Interpolated titles still come from the block parameter, so the output looks right while the first example checks the wrong value. `it_behaves_like` avoids this: each call builds its own nested subclass, so each has its own `entry`. ## shared_context versus shared_examples In rspec-core, `shared_context` and `shared_examples_for` are **aliases** of `shared_examples`. The name signals intent: - A **shared context** carries setup: `let` definitions, `before` hooks, helper `def`s. It is included with `include_context`, so the setup lands in the current group and its own examples use it directly. - **Shared examples** carry examples, usually included with `it_behaves_like` so each set gets its own group in the output. Shared contexts can also be attached automatically to every group with matching metadata; that wiring belongs to runner configuration rather than to the group DSL. ## Parameters and customization blocks Shared groups can be written two ways, and the choice changes how readable the inclusion site is: - **Block parameters**: `shared_examples "a list that deduplicates" do |item|` receives the extra arguments of `it_behaves_like`. Everything the shared group needs is visible at the call site. - **Host `let`s**: the shared block calls `list` and trusts the including group to define it. This is shorter but hides a dependency; a group that forgets the `let` fails with `NameError` from inside the shared file. - **Customization block**: `it_behaves_like "a list that deduplicates", "milk" do let(:list) { ShoppingList.new(limit: 5) } end` evaluates the block in the nested group after the shared content, overriding that `let` for this inclusion only. ## Scoping and loading - A shared group defined at the top level (`RSpec.shared_examples`) is visible everywhere. One defined inside an example group is visible to that group and its descendants, not to parents or siblings. - RSpec does not autoload shared groups. The file defining them must be loaded before the spec that uses them, typically from a support directory required in the helper file. - Including a name RSpec cannot find raises `ArgumentError` ("Could not find shared examples"), so a typo fails loudly at load time. ## Common misuse - Shared examples that silently depend on a dozen host `let`s nobody documented. Pass what they need as parameters, or fail fast with a clear message. - Using shared examples to avoid writing two lines twice. Duplication in specs is cheaper than indirection a reader must chase across files. - Choosing `include_examples` for parameterized examples included more than once in the same group.
- How can one it_behaves_like call override a let used by the shared examples without changing other inclusions?Pass a block to `it_behaves_like`. RSpec evaluates it inside the nested group it creates, after the shared content, so a `let` defined there overrides the shared or host definition for that inclusion only.
- Where must the file defining RSpec.shared_examples be loaded?Before any spec that includes it. RSpec does not autoload shared groups, so projects usually keep them in a support directory that the helper file requires; otherwise inclusion raises `ArgumentError` because the name is not registered yet.
saying these in an interview costs you the question
- include_examples creates a nested group just like it_behaves_like
- shared_context is a separate feature with its own rules
- including shared examples twice in one group keeps each let separate
- shared groups are autoloaded from the spec directory by name
- shared examples cannot receive arguments from the including group