skip to content

In an RSpec spec, how do describe, context and it relate to each other, and what does nesting groups actually build?

level: juniorimportance: must knowfreq 62%

answer

  1. groups hold examples
  2. context behaves exactly like describe
  3. a nested group is a subclass
  4. fresh group instance per example
  5. descriptions concatenate into the full name

basics

~20 s

describe and context both create an example group and behave identically; it defines one example in a group. A nested group is a subclass of its parent, inheriting let, hooks and helpers, and each example runs in a fresh group instance.

solid answer

~40 s

`RSpec.describe ShoppingList` creates an **example group**, and `it "adds an item"` defines an **example** in it. `context` is the same method under another name; by convention `describe` names a thing or method (`"#add"`) and `context` names a state (`"when the list is empty"`). Each nested group is a Ruby subclass of the outer group, so `let` definitions, `before`/`after` hooks and helper `def`s declared outside are inherited, and an inner group can redefine them. RSpec creates a new instance of the group class for every example, which is why examples do not see each other's instance variables. The full example name joins the descriptions: `ShoppingList#add when the item is already present keeps one entry`. An `it` without a block is reported as pending with "Not yet implemented".

code

bash · 10 lines
bash
$ rspec spec/shopping_list_spec.rb --format documentation

ShoppingList
  #add
    when the item is new
      stores it
    when the item is already present
      keeps one entry

2 examples, 0 failures

go deeper

for a junior

Recall that describe and context both make an example group and it makes one example, and be able to read the nested output as a sentence.

for a middle

Explain that a nested group is a subclass and each example gets a fresh instance, and use that to justify overriding let in an inner context.

for a senior

Separate load-time group-body code from per-example code, and point to where state can still leak between examples despite fresh instances.

for a principal

Set conventions for describe versus context naming so failure output reads as a specification that reviewers and newcomers can navigate.

## The three building blocks An RSpec spec file is built from two kinds of objects: - An **example group**, created by `describe` or `context`. It is a container: it holds examples, nested groups, `let` definitions, hooks and helper methods. - An **example**, created by `it` (or its aliases `specify` and `example`). It is one runnable check with a description and a block. At the top of a file the group is usually opened with `RSpec.describe ShoppingList do ... end`. Inside it, plain `describe`, `context` and `it` are available as methods of the group. | Method | Creates | Typical argument | |---|---|---| | `describe` | example group | a class, or a method name such as `"#add"` | | `context` | example group (same method, other name) | a state: `"when the list is empty"` | | `it` / `specify` / `example` | one example | the behaviour: `"keeps one entry"` | There is **no behavioural difference** between `describe` and `context`: both are generated by the same internal helper and produce the same kind of group. The split is a reading convention, so that the printed output reads as a sentence. ## What nesting builds Each group is a Ruby **class**, and a nested group is a **subclass** of the group around it. That one fact explains most of RSpec's scoping rules: 1. Everything the outer group defines (`let`, `subject`, hooks, `def` helpers, included modules) is inherited by inner groups. 2. An inner group can redefine a `let` or a helper, and the inner definition wins for examples in that group, exactly like overriding a method in a subclass. 3. Definitions in an inner group are invisible to the outer group and to sibling groups. When RSpec runs an example, it creates a **new instance of the example's group class** for that example. Instance variables set in a `before` hook or in the example body therefore live on that one instance and disappear afterwards; the next example starts clean. ```ruby RSpec.describe ShoppingList do let(:list) { ShoppingList.new } describe "#add" do context "when the item is new" do it "stores it" do list.add("milk") expect(list.items).to eq(["milk"]) end end context "when the item is already present" do before { list.add("milk") } it "keeps one entry" do list.add("milk") expect(list.items).to eq(["milk"]) end end end end ``` ## How the description is assembled RSpec builds each example's **full description** by joining the descriptions of every enclosing group with the example's own. It inserts a space between parts, except when the parent argument is a class or module and the child string starts with `#`, `.` or `::`, in which case the two are glued together. The second example above is reported as: - `ShoppingList#add when the item is already present keeps one entry` This is why the conventions matter: a failure line that reads as a sentence tells you what broke without opening the file. ## Group body versus example body Code directly inside a `describe` block runs **once, when the file is loaded**, in the context of the group class. Code inside `it`, `before` and `let` runs **later, per example**, in the context of the group instance. Mixing the two up is a common junior error: - Calling a `let` helper from the group body (for example to loop over it and generate examples) raises `RSpec::Core::ExampleGroup::WrongScopeError`, because `let` values exist only inside examples. - Calling `describe` or `it` from inside an example raises the same error from the other direction. - Plain Ruby loops in the group body over literal data are fine; they simply call `it` several times while the file loads. ## Nesting as a design tool Good nesting mirrors the questions a reader asks about the object: - One outer group per class, one `describe` per public method, one `context` per interesting state or input. - Each `context` changes exactly one thing, usually by overriding a `let` or adding a `before`, so the difference from its siblings is visible in a few lines. - Deep nesting has a cost: past three or four levels a reader must scroll up through every enclosing hook to know what state an example starts from. ## Small things interviewers check - An `it` with a description but no block is reported as **pending**, with the message "Not yet implemented", so a list of planned behaviours can be committed before it is written. - `xit`, `xdescribe` and `xcontext` mark an example or group as skipped without deleting it. - Since every example has its own instance, examples can run in any order; order-dependence only appears when state escapes through something shared, such as a global, a class-level variable or a `before(:context)` hook.

  • Why can an inner context redefine a let that the outer describe already defined?
    Each nested group is a subclass of its parent, and `let` defines a method on the group. Redefining it in the inner group overrides the method for that subclass only, so examples inside the context see the new value while siblings keep the outer one. Inside the new block, `super()` returns the outer definition.
  • What happens if you write `it "removes the item"` with no block?
    RSpec reports the example as pending with the message "Not yet implemented" rather than as a pass or a failure. It is a way to record planned behaviour; the suite stays green but the summary counts it under pending.
  • Why does calling a let helper directly inside a describe block fail?
    The group body runs once at load time on the group class, while `let` values exist only on the per-example instance. RSpec detects the call and raises `WrongScopeError`, a subclass of `NoMethodError`, explaining that the method is only available inside examples and hooks.

saying these in an interview costs you the question

  • context runs its examples in isolation, unlike describe
  • examples in one group share a single object, so instance variables carry over
  • a nested group copies the parent's let definitions and cannot override them
  • code in the describe body runs before each example
  • an it without a block counts as a passing example