You don't know RSpec: intro to RSpec for RSpec professionals
Tags: rails, rspec, testing, ruby
Published: 2024-02-01 12:54:13

## 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:

```ruby
RSpec.context User do
  describe '#first_name' do
    # ...
  end
end
```

or like this

```ruby
RSpec.example_group UserComment do
  context '#body' do
    # ...
  end
end
```

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:

```ruby
describe 'something', focus: true do #...
```

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:

```ruby
describe 'something', skip: true do # ...
```

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:

```ruby
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
```

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](https://dev.to/povilasjurcys/deep-dive-into-rspecs-let-helper-4gkk). 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:

```ruby
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
```

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:

```ruby
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
```

### `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:

```ruby
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
```

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:

```ruby
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
```

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:

```ruby
def self.subject(name = nil, &block)
  if name
    let(name, &block)
    alias_method :subject, name
  else
    let(:subject, &block)
  end
end
```

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:

```ruby
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
```

## 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!