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
or like this
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:
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:
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:
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. 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
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
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
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
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
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
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
andit
are not the only ways to start your example group or examples; - Each
describe
block is a new class that inherits from the parentdescribe
block; -
let
is a method; -
subject
is alet(: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)
Very cool. Definitely learned some new tricks.