| name | RSpec Core |
| description | This skill should be used when the user asks to "write an RSpec test", "create a spec", "use describe blocks", "set up let variables", "use before hooks", "understand RSpec structure", mentions "subject", "context", "it blocks", or needs guidance on RSpec 3.x fundamentals and test structure. |
| version | 1.0.0 |
RSpec Core Fundamentals
RSpec is a behavior-driven development (BDD) testing framework for Ruby. This skill covers the core DSL for structuring and writing specs.
Example Group Structure
describe and context
Use describe for the thing being tested (class, method, behavior). Use context for different scenarios or states:
RSpec.describe User do
describe "#full_name" do
context "when user has both names" do
it "returns first and last name" do
user = User.new(first_name: "John", last_name: "Doe")
expect(user.full_name).to eq("John Doe")
end
end
context "when user has no last name" do
it "returns only first name" do
user = User.new(first_name: "John", last_name: nil)
expect(user.full_name).to eq("John")
end
end
end
end
Naming conventions:
describewith class name:RSpec.describe Userdescribewith method:describe "#instance_method"ordescribe ".class_method"contextstarts with "when", "with", "without", "given":context "when logged in"
it and specify
Use it for individual test cases. The description should complete the sentence "it...":
it "returns the user's email" do
expect(user.email).to eq("test@example.com")
end
# For complex descriptions, use specify
specify "the total includes tax and shipping" do
expect(order.total).to eq(110.00)
end
One expectation per example (generally): Each it block should test one behavior. Multiple expectations are acceptable when testing related aspects of a single behavior.
Subject and Let
subject
Defines the primary object under test:
RSpec.describe User do
subject { User.new(name: "John") }
it { is_expected.to be_valid }
# Named subject for clarity
subject(:user) { User.new(name: "John") }
it "has the correct name" do
expect(user.name).to eq("John")
end
end
let and let!
Use let for lazy-evaluated memoized helpers:
RSpec.describe Order do
let(:user) { User.create!(name: "John") }
let(:product) { Product.create!(name: "Widget", price: 10) }
let(:order) { Order.new(user: user, product: product) }
it "calculates total correctly" do
expect(order.total).to eq(10)
end
end
let vs let!:
let- Lazy evaluation, created when first referencedlet!- Eager evaluation, created before each example
let(:user) { User.create!(name: "John") } # Created only if referenced
let!(:admin) { User.create!(admin: true) } # Always created before each test
When to use let!:
- Database records that must exist for callbacks/associations
- Setup that has side effects needed by other code
- When order of creation matters
Hooks
before and after
Execute code before or after examples:
RSpec.describe User do
before(:each) do # or just `before do`
@user = User.new
end
after(:each) do
# Cleanup after each example
end
before(:all) do # or before(:context)
# Run once before all examples in this group
# Be careful: shared state between examples
end
after(:all) do
# Run once after all examples
end
end
Hook execution order:
before(:all)- once per describe/context blockbefore(:each)- before every example- The example runs
after(:each)- after every exampleafter(:all)- once after all examples
around
Wrap examples with custom logic:
around(:each) do |example|
Timecop.freeze(Time.now) do
example.run
end
end
# Useful for database transactions
around(:each) do |example|
ActiveRecord::Base.transaction do
example.run
raise ActiveRecord::Rollback
end
end
Pending and Skipping
pending
Mark examples as not yet implemented or temporarily broken:
it "sends welcome email" do
pending "email service not implemented"
expect(user.send_welcome_email).to be_truthy
end
# Or mark entire example as pending
pending "calculates complex tax" do
expect(order.tax).to eq(15.00)
end
skip
Skip examples entirely:
it "requires external service", skip: "API not available in test" do
# This won't run
end
# Conditional skip
it "runs on CI only", skip: !ENV["CI"] do
# Skipped locally
end
# Skip with xit
xit "not implemented yet" do
# Skipped
end
Metadata and Filtering
Add metadata to examples and filter runs:
RSpec.describe User, type: :model do
it "validates email", :slow do
# Tagged with :slow
end
it "processes payment", :integration, timeout: 30 do
# Multiple tags
end
end
# Run filtered specs:
# rspec --tag slow
# rspec --tag ~slow (exclude slow)
# rspec --tag type:model
Focus
Temporarily run only specific examples:
fit "only this test runs" do # focus + it
expect(true).to be true
end
fdescribe User do # focus + describe
# Only examples in this block run
end
fcontext "when focused" do # focus + context
end
Important: Remove focus tags before committing. Configure CI to fail on focus:
# spec/spec_helper.rb
RSpec.configure do |config|
config.filter_run_when_matching :focus
config.run_all_when_everything_filtered = true
end
Configuration
spec_helper.rb vs rails_helper.rb
spec_helper.rb- Pure RSpec configuration, no Railsrails_helper.rb- Rails-specific setup, requires spec_helper
# spec/spec_helper.rb
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = "spec/examples.txt"
config.disable_monkey_patching!
config.order = :random
Kernel.srand config.seed
end
Best Practices
Describe What, Not How
# Good - describes behavior
it "returns user's full name" do
expect(user.full_name).to eq("John Doe")
end
# Avoid - describes implementation
it "concatenates first_name and last_name with space" do
expect(user.full_name).to eq("John Doe")
end
Use Descriptive Context Names
# Good
context "when user is an admin"
context "with valid attributes"
context "without a password"
# Avoid
context "admin case"
context "valid"
context "test 1"
Prefer let Over Instance Variables
# Good
let(:user) { create(:user) }
# Avoid
before { @user = create(:user) }
Keep Examples Independent
Each example should be able to run in isolation. Avoid dependencies between examples.
Additional Resources
Reference Files
For advanced patterns and detailed examples:
references/hooks-deep-dive.md- Comprehensive hook patterns and execution orderreferences/configuration.md- Full RSpec configuration options
Example Files
Working examples in examples/:
examples/model_spec.rb- Complete model spec exampleexamples/service_spec.rb- Service object testing patterns