
Well-organized tests are easier to read, understand, and maintain. This lesson will guide you through the fundamental concepts of organizing your RSpec tests, focusing on example groups, shared setup code, and metadata.
Why Structure Matters
Imagine trying to find a specific book in a library where all the books are just piled up randomly. It would be a nightmare, right? Similarly, if your tests are not well-structured, it becomes difficult to understand what each test is doing, and it’s hard to maintain them as your application grows.
RSpec provides tools to organize your tests into logical groups, making them easier to read and maintain. This lesson will cover the following:
- Example Groups: How to group your tests logically.
- Shared Setup Code: How to avoid repeating setup code in multiple tests.
- Metadata: How to add extra information to your tests.
Example Groups: The Foundation of RSpec Tests
In RSpec, every test (or “spec”) is part of an example group. Think of example groups as containers that hold related tests. They serve several purposes:
- Logical Structure: They help you organize your tests based on the feature or functionality you’re testing.
- Context: They describe the context of what you’re testing.
- Scope: They act as a scope for shared logic, like setup code or helper methods.
- Setup/Teardown: They allow you to run common setup and teardown code.
describe and it
The two most basic building blocks of RSpec tests are describe
and it
:
- describe: Creates an example group. It’s used to specify what you are testing.
- it: Creates a single example (a test) within an example group. It’s used to specify how something should behave.
Let’s look at some examples in the context of an LMS application:
# spec/models/course_spec.rb
RSpec.describe Course do # Example group for the Course model
it 'has a title' do # Example within the Course group
course = Course.new(title: 'Introduction to Ruby')
expect(course.title).to eq('Introduction to Ruby')
end
it 'has a description' do
course = Course.new(description: 'Learn the basics of Ruby programming.')
expect(course.description).to eq('Learn the basics of Ruby programming.')
end
end
In this example:
RSpec.describe Course do
creates an example group for the Course model.it 'has a title' do
andit 'has a description' do
are individual tests within the Course group.
describe in Detail
The describe
method can take different types of arguments:
- String: A descriptive string of what you’re testing.
- Ruby Class/Module/Object: The class, module, or object you’re testing.
- Combination: A class/module/object and a string.
RSpec.describe 'Course Management API' do # String description
end
RSpec.describe Course do # Class name
end
RSpec.describe Course, 'when a student enrolls' do # Class name and string
end
Using a class name is beneficial because it helps catch spelling mistakes and allows for RSpec extensions.
You can also add metadata to describe blocks:
RSpec.describe Course, 'with quizzes', feature: :quizzes do
end
it in Detail
The it
method takes a description of the behavior being specified. It can also take custom metadata:
RSpec.describe Assignment do
it 'can be graded', priority: :high do
end
end
Other Ways to Get the Words Right
RSpec provides aliases for describe
and it
to improve readability.
context Instead of describe
context
is an alias for describe
, but it’s used to group examples related to a shared situation or condition. It reads better in certain contexts.
RSpec.describe Course do
context 'when a student is enrolled' do
it 'allows the student to access course materials'
it 'allows the student to submit assignments'
end
end
example Instead of it
example
is an alias for it
, but it’s used when providing data examples rather than sentences about a subject.
RSpec.describe GradeCalculator, 'calculates grades' do
example 'for a student with 90 points'
example 'for a student with 75 points'
end
specify Instead of it
specify
is used when examples have their own subject, not a common one. It’s useful for cross-cutting concerns.
RSpec.describe 'User Authentication' do
specify 'users can log in with valid credentials'
specify 'users cannot log in with invalid credentials'
end
Defining Your Own Names
You can define custom aliases for describe
and it
. For example, you can create pdescribe
and pit
for debugging with Pry:
RSpec.configure do |rspec|
rspec.alias_example_group_to :pdescribe, pry: true
rspec.alias_example_to :pit, pry: true
rspec.after(:example, pry: true) do |ex|
require 'pry'
binding.pry
end
end
Now, you can use pdescribe
and pit
to trigger Pry breakpoints in your tests.
Sharing Common Logic

To avoid repeating code, RSpec provides several techniques for sharing setup logic:
- let Definitions: For simple initializations.
- Hooks (before, after, around): For more complex setup and teardown.
- Helper Methods: For reusable logic.
let Definitions
let
is great for setting up anything that can be initialized in a line or two of code. It provides lazy evaluation (the code is only executed when the variable is used).
let(:student) { Student.new(name: 'Alice') }
let(:course) { Course.new(title: 'Advanced Ruby') }
it 'allows a student to enroll in a course' do
student.enroll(course)
expect(student.courses).to include(course)
end
Hooks
Hooks are used when let
blocks are insufficient (e.g., side effects, database transactions).
- before: Runs before each example.
- after: Runs after each example (even if it fails).
- around: Wraps examples, running code before and after.
before and after
RSpec.describe Assignment do
before(:example) do
@original_time = Time.now
end
after(:example) do
Time.stub(:now, @original_time)
end
it 'has a due date' do
assignment = Assignment.new(due_date: Time.now + 7.days)
expect(assignment.due_date).to be > Time.now
end
end
Tip: Favor before
over after
for data cleanup to ensure failures happen in the correct example.
around
RSpec.describe Assignment do
around(:example) do |ex|
original_time = Time.now
ex.run
Time.stub(:now, original_time)
end
it 'has a due date' do
assignment = Assignment.new(due_date: Time.now + 7.days)
expect(assignment.due_date).to be > Time.now
end
end
Config Hooks
Hooks can be defined in RSpec.configure
for the entire suite.
RSpec.configure do |config|
config.around(:example) do |ex|
original_time = Time.now
ex.run
Time.stub(:now, original_time)
end
end
Important: Use config hooks for incidental details, not essential logic.
Scope
Hooks can run once per example (:example
), once per group (:context
), or once per suite (:suite
).
:example
is the default scope.:context
is used for time-intensive operations that don’t interact with per-example lifecycles.:suite
hooks run once before the entire test suite.
RSpec.describe 'Course Management System' do
before(:context) do
Database.connect
end
after(:context) do
Database.disconnect
end
end
RSpec.configure do |config|
config.before(:suite) do
FileUtils.rm_rf('tmp')
end
end
Warning: Use :context
hooks cautiously, as they can cause issues with database code.
When to Use Hooks
Hooks should:
- Remove duplicated or incidental details.
- Express English descriptions as executable code.
Avoid overusing hooks, as it can make tracing program flow difficult.
Helper Methods
Example groups are Ruby classes, so you can define helper methods.
RSpec.describe GradeCalculator do
def calculate_grade(score)
GradeCalculator.new.calculate(score)
end
it 'returns A for a score of 90' do
expect(calculate_grade(90)).to eq('A')
end
end
Important: Helper methods should make it explicit what behavior is being tested.
Putting Helpers in a Module
Helper methods can be defined in a separate module and included in example groups.
module LMSHelpers
def create_student(name)
Student.new(name: name)
end
end
RSpec.describe Course do
include LMSHelpers
it 'allows a student to enroll' do
student = create_student('Bob')
# ...
end
end
Including Modules Automatically
Use config.include
in RSpec.configure
to include a module in all example groups.
RSpec.configure do |config|
config.include LMSHelpers
end
Warning: Avoid hiding important details behind config.include
.
Sharing Example Groups
Shared example groups allow reusing examples, let
constructs, and hooks.
- shared_context and include_context: For reusing setup and helper logic.
- shared_examples and include_examples: For reusing examples.
- it_behaves_like: Creates a new, nested example group to hold shared code.
Sharing Contexts
RSpec.shared_context 'student setup' do
let(:student) { Student.new(name: 'Alice') }
before { @original_time = Time.now }
after { Time.stub(:now, @original_time) }
end
RSpec.describe Course do
include_context 'student setup'
it 'allows a student to enroll' do
# ...
end
end
Sharing Examples
RSpec.shared_examples 'a model with a title' do
it 'has a title' do
expect(subject.title).to be_a(String)
end
end
RSpec.describe Course do
subject { Course.new(title: 'Ruby Basics') }
it_behaves_like 'a model with a title'
end
RSpec.describe Assignment do
subject { Assignment.new(title: 'Homework 1') }
it_behaves_like 'a model with a title'
end
Note: Shared examples are typically placed in spec/support
.
Nesting
include_examples
pastes the shared examples directly into the describe
block, which can cause conflicts. it_behaves_like
nests the shared examples in their own context, avoiding conflicts.
Tip: When in doubt, choose it_behaves_like
.
Customizing Shared Groups With Blocks
RSpec.describe Course do
it_behaves_like 'a model with a title' do
subject { Course.new(title: 'Advanced Ruby') }
end
end