DEV Community

Cover image for You don't know RSpec: intro to RSpec for RSpec professionals
Povilas Jurčys
Povilas Jurčys

Posted on

You don't know RSpec: intro to RSpec for RSpec professionals

Intro

Welcome to a journey through RSpec's fundamental tools! RSpec is a popular testing framework for Ruby, designed to make writing and maintaining tests more intuitive and efficient. The majority of developers are already familiar with the basic RSpec helpers such as describe, it, let, and subject.

In this article, we will focus on each of these seemingly simple yet powerful methods, uncovering their nuanced functionalities. Prepare to be astonished as we unveil the richness of RSpec's toolkit, empowering you to write tests that not only validate your code but also enhance your development experience.

Do you know describe?

Like Big Bang starts with a BANG!, so RSpec description starts with the RSpec.describe. But do you know that there are 7 alternative ways you can "describe" your example group? 3 of them are identical and might be more familiar to you:

  • describe
  • context
  • example_group

The other 4 describe-like methods not only describe example groups but also add additional metadata. These are:

  • fdescribe
  • fcontext
  • xdescribe
  • xcontext

Let's focus on each of those groups separately.

describe, context, and example_group

You read it right - context and describe (and less known example_group) from the technical perspective are the same. It has differences for us, developers only. describe is mostly used to specify the class or method you want to test, and context is used for defining different conditions in which the method is tested.

So, everything you can do with describe, you can also do with context or example_group. For instance, you can initiate your spec with a context instead of describe like this:

RSpec.context User do
  describe '#first_name' do
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

or like this

RSpec.example_group UserComment do
  context '#body' do
    # ...
  end
end
Enter fullscreen mode Exit fullscreen mode

I do not recommend doing so, but it's nice to know the possibilities.

fdescribe and fcontext

Prefix f in fdescribe and fcontext means "focus". It's a shortcut for:

describe 'something', focus: true do #...
Enter fullscreen mode Exit fullscreen mode

If you have fdescribe in your spec file, the rspec command will run tests only in the fdescribe group and skip other specs.

xdescribe and xcontext

Prefix 'x' in xdescribe and xcontext means that the given example block will be skipped. It's a shortcut for:

describe 'something', skip: true do # ...
Enter fullscreen mode Exit fullscreen mode

The rspec command will run all specs except those defined in the example group described with xdescribe or xcontext.

Do you know it?

If the idea of having 7 default ways to describe your spec seems overwhelming, prepare to be amazed: it has 12! Here is a list of all methods, along with their aliases grouped together:

  • it, specify, example
  • focus, fit, fspecify, fexample
  • skip, xit, xspecify, xexample
  • pending

As with describe there are 3 tag-free aliases for defining example blocks. Then there are some aliases for "focus" and "skip" tags. Then there is a pending that runs your test example without reporting any failures.

Custom aliases for describe and it

You are now familiar with many aliases for starting your describe and it block. But you can go even further and create custom aliases. Here is how to do it:

RSpec.configure do |config|
  config.alias_example_group_to :having, my_tag: true
  config.alias_example_to :i_would
end

RSpec.describe Foo do
  having 'my custom describe alias' do
    i_would { expect(:life).to be(:easy) }
  end
end
Enter fullscreen mode Exit fullscreen mode

This opens a new world of possibilities to have specs tailored to your needs and business language. Even though: don't overuse it.

Do you know let?

let looks so simple, but its shy look hides complexities many of us can't imagine. I wrote a dedicated post just for let called Deep Dive into RSpec's let Helper. When we work with let daily we do not hesitate to ask: "What is let?". Like, yeah, it's a helper, but... What is let from the ruby perspective? Is it a variable? Lambda?

The answer is: let is a method.

And why is that important? Because you can call super() in let and it supports all the context/describe hierarchy. Let me explain this in rspec language:

RSpec.describe 'Playing with `let`' do
  let(:foo) { 'foo' }

  it { expect(foo).to eq 'foo' }

  context 'with capitalized value' do
    let(:foo) { super().upcase }

    it { expect(foo).to eq('FOO')
  end
end
Enter fullscreen mode Exit fullscreen mode

As you can see, we redefined the second let(:foo) using super(). This nice technique is handy when you have a huge initial let Hash and you want to test cases with tiny modifications. For example, in controller tests when you send a lot of params and you want to test what will happen when you send the same data but with one missing value?

But there is another interesting side effect of let being a method. You can use let and def interchangeably. So we can write previous spec like this too:

RSpec.describe 'let vs def' do
  let(:foo) { 'foo' }

  it { expect(foo).to eq 'foo' }

  context 'with capitalized value' do
    def foo
      super().upcase
    end

    it { expect(foo).to eq('FOO')
  end
end
Enter fullscreen mode Exit fullscreen mode

super() and describe

So let is a method that supports super(). When you use super() within a let block, it retrieves the value from the parent describe block. This hints at something crucial: underneath, each describe block is executed in the class context that inherits from its parent describe block. This insight sheds light on the powerful inheritance mechanisms at play within RSpec's structure, offering a deeper understanding of how specifications are organized and executed.

Do you know subject?

Have you ever wondered if named subject is the same as let? How about subject without the name? The short and boring answer is yes, in many cases the subject is the same as let. If you are curious about those cases, when the subject is not the same - read on!

Unnamed subject VS let

subject { ... } without the name is simply shortcut for let(:subject) { ... }. What it means, is that you can write this and it will work:

RSpec.describe 'subject vs let' do
  subject { 'foo' }

  it { expect(foo).to eq 'foo' }

  context 'with super in let(:subject)' do
    let(:subject) { super().upcase }

    it { expect(subject).to eq('FOO')
  end

  context 'with super in subject' do
    subject { super().upcase }

    it { expect(subject).to eq('FOO')
  end
end
Enter fullscreen mode Exit fullscreen mode

Mindblowing!

But things become much more complicated when you add the name to your subject

Complexities of the named subject

However, there is one limitation: you are not allowed to call super() in a named subject. So this will not work:

RSpec.describe 'named subject' do
  subject(:foo) { 'foo' }

  it { expect(foo).to eq 'foo' }

  context 'with super in subject' do
    subject(:foo) { super().upcase }

    it { expect(foo).to eq('FOO') } # will raise error
  end
end
Enter fullscreen mode Exit fullscreen mode

But why super() in the named subject is much more complex than in the unnamed subject? This is a simplified subject definition you can find in the rspec-core gem:

def self.subject(name = nil, &block)
  if name
    let(name, &block)
    alias_method :subject, name
  else
    let(:subject, &block)
  end
end
Enter fullscreen mode Exit fullscreen mode

So named subject creates two methods. And when you call super() it's become very confusing because it's unclear which method should receive super(). But you can create let with the name matching named subject name. Or if you want to be even more hacky, you can define let(:subject) and call super() there. It will work, but keep in mind that it will modify only one of the two methods. Let me show you this with RSpec example:

RSpec.describe 'named subject' do
  subject(:foo) { 'foo' }

  it { expect(foo).to eq 'foo' }

  context 'with super in let(:foo)' do
    let(:foo) { super().upcase }

    it { expect(foo).to eq('FOO') }
    it { expect(subject).to eq('foo') }
  end

  context 'with super in let(:subject)' do
    let(:subject) { super().upcase }

    it { expect(foo).to eq('foo') }
    it { expect(subject).to eq('FOO') }
  end
end
Enter fullscreen mode Exit fullscreen mode

Summary

Our journey through RSpec's fundamental tools has been truly remarkable! I trust you enjoyed it as much as I did. Let's take a moment to revisit the key points covered in this post:

  • describe and it are not the only ways to start your example group or examples;
  • Each describe block is a new class that inherits from the parent describe block;
  • let is a method;
  • subject is a let(:subject).

Now that we've got all these new ideas in our heads, let's use them to go on even more exciting testing adventures!

Top comments (1)

Collapse
 
kevgathuku profile image
Kevin Gathuku

Very cool. Definitely learned some new tricks.