- Published on
How to Write a Spec with RSpec
- Authors
- Name
- Manuel Sousa
- @mlrcbsousa
Testing your code effectively is crucial to maintaining the stability and reliability of your software. One of the most popular tools for writing tests in Ruby is RSpec. This post provides a step-by-step guide on how to write a well-structured spec, complete with annotated code snippets and detailed explanations.
RSpec 101: A Practical Guide
Running a Spec
Before diving into writing specs, here’s a simple command to run your tests:
rspec spec/path/to/file_spec.rb
A single line of code like this can test entire files or specific specs. Remember, concise and clear commands are key to efficient testing.
Anatomy of a Spec
Let's break down an annotated RSpec spec and understand the syntax and structure. Note that some parts have been omitted or simplified for clarity.
# spec/channels/application_cable/connection_spec.rb
# Always require 'rails_helper' at the top
require 'rails_helper.rb'
RSpec.describe ApplicationCable::Connection, type: :channel do
# Top-level `before` block for general setup
before do
allow(described_class).to receive(:env).and_return(env)
...
end
context 'with a verified user' do
# Nested `before` blocks to handle specific scenarios
before { exec_command }
it "successfully connects" do
...
end
end
context 'without a verified user' do
# Using lazy loading for variables
let(:user) { nil }
let(:headers) { {} }
it "rejects connection" do
expect(exec_command_proc).to have_rejected_connection
end
end
# Always use `private` to separate helper methods from the main spec
private
# Define methods before variable assignments for clarity
def exec_command
connect "/cable", headers: headers
end
def exec_command_proc
method(:exec_command).to_proc
end
# Lazy loaded variable definitions
let(:user) { instance_double(User, id: id) }
let(:env) { instance_double('env') }
let(:headers) { { "X-USER-ID" => id } }
end
Key Concepts
describe
and context
Start your spec with an RSpec.describe
block, which describes the object or method you're testing. Use describe
for methods and context
to specify different scenarios. While they function the same way, using both enhances readability.
Convention Tip: Use describe '#instance_method'
for instance methods and describe '.class_method'
for class methods.
described_class
Refer to the class under test using described_class
. This improves readability and makes your tests more maintainable.
In the example above:
RSpec.describe ApplicationCable::Connection, type: :channel do
before do
allow(described_class).to receive(:env).and_return(env)
...
Would be equivalent to:
RSpec.describe ApplicationCable::Connection, type: :channel do
before do
allow(ApplicationCable::Connection).to receive(:env).and_return(env)
...
When not testing a class, use a string like for example:
RSpec.describe 'Some Module - Update API' do
# Use described_class instead of repeating the module or class name
end
type
Specify the type of spec with the type:
argument. This activates special RSpec DSLs, like :channel
for Action Cable channels. You can also add metadata like skip: true
or custom tags for more control.
Common Blocks: subject
and before
subject
subject
is similar to a let
block but used for the primary object under test. It simplifies expectations with a more natural syntax.
it 'has a result of zero' do
expect(something).to be_zero
end
becomes:
subject { something }
it 'is expected to be zero' do
is_expected.to be_zero
end
before
Use before
for setup and after
for teardown. Both can take arguments, like before(:each)
or after(:all)
. :each
is the default, running before each it
block.
Lazy Loading with let
and let!
let
is used for defining variables lazily. They’re only instantiated when referenced, keeping your specs clean. Use let!
if you need eager loading.
let(:variable) { 'something' }
This keeps your spec clean and optimizes performance. Avoid direct variable assignment in specs.
Making Assertions: it
and expect
The it
block is where you make assertions using expect
. Write readable, human-like sentences to make your specs understandable to others.
it 'returns a valid response' do
expect(response).to be_successful
end
Writing Helper Methods
Place helper methods at the bottom of your spec, under private
, along with let
assignments to keep everything organized.
Mocks, Stubs, and Spies
Use allow
to create stubs:
allow(described_class).to receive(:env).and_return(env)
For stubbing objects, use instance_double
:
let(:user) { instance_double(User, id: id) }
Instance doubles mimic real objects, responding to methods you specify. For more control, you can use spy
to track interactions.
Wrapper Methods: exec_command
The exec_command
and exec_command_proc
methods standardize how actions are executed in your tests. These are personal preferences and not part of the RSpec framework. exec_command_proc
allows you to test if a command raises an error:
it 'raises an error' do
expect(exec_command_proc).to raise_error
end
Conclusion
This guide provides a concise yet comprehensive look at writing effective RSpec tests. By structuring your specs well, you not only ensure your code behaves as expected but also make your tests readable and maintainable for your team.