Spec-tacular: ways to think about writing expressive RSpec

Posted by on 7 July 2026

I’ve always enjoyed the interface given to us by RSpec. RSpec, when used to its fullest, gives us ways to extract out test setup and teardown, and draw focus to how the output of the test should change given the variable we’re changing. I tend to think about this terms of scientific testing:

RSpec.describe VatReturn do
  # This, and any other setup we do in the top-level `describe` block
  # or the `#can_be_filed?` `describe` block, sets out the global
  # state – these would be considered control variables.
  let(:vat_return) { described_class.new(filed:) }

  describe "#can_be_filed?" do
    # This defines the dependent variable: the thing we
    # expect to change.
    subject { vat_return.can_be_filed? }

    context "when the VAT return has already been filed" do
      # This is the independent variable: the one thing we
      # intentionally change.
      let(:filed) { true }
      
      # We document our expectation here in the test.
      it { is_expected.to be false }
    end
  end
end

When done correctly, this can be really illustrative of a class’s behaviour. While the widespread nature of RSpec wasn’t really intended by its original creator, I think the interface it gives us is maybe one of the most beautiful things to come out of Ruby. For that reason, I’ve always been partial to using what it gives us to the fullest, rather than including all four phases of a test in one example block.

One significant part of this style is the combination of subject and is_expectedsubject sets up a variable subject in the same way that let does, and is_expected acts as a shortcut for expect(subject) . In the test above, it { is_expected.to be false } calls the block passed to subject, and checks that its return value is false.

On its own, it { is_expected.to be false } reads like a sentence. RSpec builds up what it calls an implicit description for the test at runtime, which you can see when running RSpec using the --format documentation option. In this case, it’s quite nicely able to derive a sentence from this: VatReturn#can_be_filed? when the VAT return has already been filed is expected to be false.

That sentence tells you what the return value of the method is under those conditions. It describes the interface presented by the class. However, it’s not always the perfect framing for behaviour.

Dealing with views

The testing style we had back there works nicely for plain old Ruby objects (POROs). If what we’re doing is as simple as calling a method and checking its return value, then RSpec is a great fit for documenting that interface, when used as I’ve described. But many things in Rails aren’t quite that straightforward, such as views.

A view isn’t a class. A view is a template that’s rendered by a controller. View specs generally try to forge the idea of a “return value” by saying that the subject is the HTML output of rendering the view. That means you might specify your subject as:

subject do
  render    # Renders the view
  rendered  # The output of doing so is saved to this helper method.
end

subject then contains that HTML output immediately after rendering the view, which means you can write expectations like:

it { is_expected.to have_css("[data-testid='...']") }

It would follow, then, that the description for the example would be it is expected to have css [data-testid="..."]. It does document the interface – when we render the view under these conditions, it does include an element with the test ID we’re looking for. But that’s not immediately illustrative of what the view is rendering. Even if we choose a descriptive name for our test ID, the implicit description doesn’t read as nicely as it should. In this scenario, we’d prefer to phrase things in terms of the behaviour.

We might instead want to say “it is expected to include the delete button”, for example. Referring to an element by its CSS is a layer of abstraction away from what the element is actually doing on the page – we’re not particularly interested in what attributes the element has, we want to know what the element is for. In that case, we’d want to specify our own description:

it "includes the delete button" do
  expect(subject).to have_css("[data-testid='...']")
end

At that point, we’ve lost a bit of the brevity given to us by implicit example descriptions, but it’s worth the cost – we’re framing this example in a way that’s more suited to how we’d expect a view to be described. If it’s regularly optimal to write our own description over using an implicit one, it’s a signal that the scenario we’re dealing with calls for things to be expressed in terms of behaviour.

Dealing with controllers

Changing the subject

When testing controllers, we’d similarly want to build up a subject from calling multiple methods. Our subject might be something like:

subject do
  get :new # Make a request to the route that triggers the `#new` action
  response # Contains the response from that request
end

response is the perfect name for a subject if we do want to use a named subject – which we likely do if we want to make multiple assertions. Since response is usually defined as a test helper already, we could use the underlying instance variable in this case to safely set up a named subject called response:

subject(:response) do
  get :new
  @response
end

That’ll look great in a test like:

it do
  expect(response).to(
    have_http_status(:forbidden).and(
      render_template("forbidden")
    )
  )
end

We’re achieving a few things here:

  • we’re using multiple expectations in one spec, so we’re only making the request once
  • we’re reusing the response variable name, which is perfectly expressive
  • we’re still keeping the request in subject so that it can be reused

When would you change the description?

We might also want to phrase the test description in terms of behaviour, but it depends. Sometimes the description might be too verbose by default, or we might want to more elegantly describe a create/read/update/delete action taken against a record.

In the example above, the HTTP status and the template that is rendered nicely describe the behaviour of the controller action – we’re presenting the user with an error status and a view that indicates to them that they’re not allowed to access this area. But if we were testing a validation response from a form, something like:

it do
  expect(response).to(
    have_http_status(:unprocessable_entity).and(
      render_template("edit").and(
        have_attributes(body: include(params[:invoice][:name]))
      )
    )
  )
end

…then it’s not as immediately clear why we’re re-rendering the edit page on a 422 without an additional mental hop to say “that’s how we typically respond in case of a validation error in Rails.” So this is another case where we might benefit from setting a manual description to describe the behaviour – something like it "rerenders the edit form with the invoice details".

Why wouldn’t you use this?

I’m keen on this style myself, but over time I’ve seen some valid objections to it. Murad Iusufov raised an excellent case for using Minitest over RSpec – the highlight of that talk was really that Minitest makes it easier to write a cleaner four-phase test by encouraging you to make use of what Ruby already has (such as private methods), which can be useful for navigating some of the nesting that comes with RSpec. I think that RSpec’s tendency for nesting is fine if you go no more than two contexts deep:

# Setup omitted for brevity
RSpec.describe DeanMartin do
  describe "#amore?" do
    context "when the moon hits your eye like a big pizza pie" do
      context "when the world seems to shine like you've had too much wine" do
        it { is_expected.to be true }

As I illustrated at the start of this post – and feel free to disagree with me here! – that same nesting really helps to organise tests and lets us focus on the independent variables local to each example. That does, however, come with a complexity cost – good RSpec hygiene is something that needs to be taught, and that’s not currently done comprehensively as far as I’ve seen, though betterspecs.org makes some good syntactic suggestions. If I were advising on a brand-new Rails codebase, I would say that the expressiveness that comes with RSpec is well worth it if you’re committed to the idea of tests as documentation.


RSpec is immensely useful in documenting your code through tests if you make full use of its functionality and take a scientific approach to organising your tests. Thanks to its many matchers, you can usually let RSpec set an example description for you – but needing to describe behaviour as opposed to an interface is a good litmus test for whether you need to step in with your own string.

Leave a reply

Your email address will not be published. Required fields are marked *