- 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.rbA 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 } }
endKey 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
endtype
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
endbecomes:
subject { something }
it 'is expected to be zero' do
is_expected.to be_zero
endbefore
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
endWriting 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
endConclusion
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.