| name | spec-organization |
| description | This skill should be used when the user asks about "shared examples", "shared contexts", "spec structure", "test organization", "describe blocks", "RSpec tagging", "spec file layout", or needs guidance on organizing and structuring RSpec test suites. |
| version | 1.0.0 |
Spec Organization
Well-organized specs are easier to maintain, faster to run, and clearer to read. This skill covers patterns for structuring RSpec test suites.
Directory Structure
Standard Rails RSpec layout:
spec/
├── spec_helper.rb # Pure RSpec config
├── rails_helper.rb # Rails-specific config
├── support/ # Shared helpers and config
│ ├── factory_bot.rb
│ ├── capybara.rb
│ └── shared_examples/
├── factories/ # Factory Bot definitions
│ ├── users.rb
│ └── posts.rb
├── models/ # Model specs
├── requests/ # Request specs (API)
├── system/ # Browser/system specs
├── services/ # Service object specs
├── jobs/ # Background job specs
├── mailers/ # Mailer specs
└── lib/ # Library code specs
Shared Examples
Reuse test logic across multiple specs:
Defining Shared Examples
RSpec.shared_examples "soft deletable" do
describe "#soft_delete" do
it "sets deleted_at timestamp" do
expect { subject.soft_delete }.to change(subject, :deleted_at).from(nil)
end
it "does not actually destroy record" do
subject.soft_delete
expect(described_class.unscoped.find(subject.id)).to be_present
end
end
describe "#deleted?" do
it "returns true when soft deleted" do
subject.soft_delete
expect(subject).to be_deleted
end
it "returns false when not deleted" do
expect(subject).not_to be_deleted
end
end
end
Using Shared Examples
RSpec.describe User, type: :model do
subject { create(:user) }
it_behaves_like "soft deletable"
end
RSpec.describe Post, type: :model do
subject { create(:post) }
it_behaves_like "soft deletable"
end
Shared Examples with Parameters
RSpec.shared_examples "publishable" do |factory_name|
let(:resource) { create(factory_name) }
describe "#publish" do
it "sets published_at" do
resource.publish
expect(resource.published_at).to be_present
end
end
end
it_behaves_like "publishable", :post
it_behaves_like "publishable", :article
Block Form for Context
RSpec.shared_examples "requires authentication" do
context "when not authenticated" do
before { sign_out }
it "redirects to login" do
subject
expect(response).to redirect_to(login_path)
end
end
end
it_behaves_like "requires authentication" do
subject { get protected_path }
end
Shared Contexts
Share setup across specs:
Defining Shared Context
RSpec.shared_context "authenticated user" do
let(:current_user) { create(:user) }
before do
sign_in current_user
end
end
RSpec.shared_context "with admin user" do |role|
let(:current_user) { create(:user, role: role || :admin) }
before do
sign_in current_user
end
end
Using Shared Context
RSpec.describe "Dashboard", type: :request do
include_context "authenticated user"
it "shows user dashboard" do
get dashboard_path
expect(response).to have_http_status(:ok)
end
end
RSpec.configure do |config|
config.include_context "authenticated user", authenticated: true
end
RSpec.describe "Admin Panel", type: :request, authenticated: true do
end
Tagging and Filtering
Adding Tags
RSpec.describe User, type: :model do
it "validates email", :slow do
end
it "sends notification", :external, :email do
end
context "with external API", :integration do
end
end
Running with Tags
rspec --tag slow
rspec --tag integration
rspec --tag type:model
rspec --tag ~slow
rspec --tag ~external
rspec --tag slow --tag integration
Metadata Configuration
RSpec.configure do |config|
config.filter_run_excluding :slow unless ENV["CI"]
config.include ApiHelpers, type: :request
config.include SystemHelpers, type: :system
config.around(:each, :freeze_time) do |example|
travel_to(Time.zone.local(2024, 1, 1)) { example.run }
end
end
Custom Matchers in Support
RSpec::Matchers.define :have_json_key do |expected|
match do |response|
body = JSON.parse(response.body)
body.key?(expected.to_s)
end
failure_message do |response|
"expected response to have JSON key '#{expected}'"
end
end
expect(response).to have_json_key(:data)
Helper Modules
module AuthenticationHelpers
def sign_in_as(user)
post login_path, params: { email: user.email, password: "password" }
end
def current_user
User.find(session[:user_id])
end
end
RSpec.configure do |config|
config.include AuthenticationHelpers, type: :request
config.include AuthenticationHelpers, type: :system
end
Describe and Context Best Practices
Describe Block Naming
RSpec.describe User do
describe ".class_method" do
end
describe "#instance_method" do
end
describe "validations" do
end
end
Context Block Naming
Start with "when", "with", "without", "given":
context "when user is an admin" do
end
context "with valid attributes" do
end
context "without a password" do
end
context "given a holiday schedule" do
end
Nested Context Guidelines
Keep nesting to 3 levels maximum:
RSpec.describe Order do
describe "#complete" do
context "when payment succeeds" do
it "marks order as completed" do
end
end
context "when payment fails" do
it "keeps order pending" do
end
end
end
end
RSpec.describe Order do
context "when user is logged in" do
context "when cart has items" do
context "when payment method is credit card" do
context "when card is valid" do
end
end
end
end
end
File Organization Patterns
One Spec File Per Class
Spec Mirrors App Structure
app/models/user.rb → spec/models/user_spec.rb
app/services/orders/create.rb → spec/services/orders/create_spec.rb
app/controllers/api/v1/users_controller.rb → spec/requests/api/v1/users_spec.rb
Feature-Based Organization (Alternative)
spec/
├── features/
│ ├── authentication/
│ │ ├── login_spec.rb
│ │ └── logout_spec.rb
│ └── checkout/
│ ├── add_to_cart_spec.rb
│ └── payment_spec.rb