This skill should be used when the user asks about "RSpec", "rspec-rails", "spec files", "describe blocks", "it blocks", "let", "let!", "subject", "shared examples", "FactoryBot", "factories", "shoulda-matchers", "request specs", "system specs", "feature specs", "model specs", "BDD", or needs guidance on testing Rails applications with RSpec.
/plugin marketplace add bastos/rails-plugin/plugin install bastos-ruby-on-rails@bastos/rails-pluginThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/rspec-patterns.mdComprehensive guide to testing Rails applications with RSpec, the behavior-driven development (BDD) testing framework.
# Gemfile
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
end
group :test do
gem 'shoulda-matchers'
gem 'capybara'
gem 'selenium-webdriver'
end
rails generate rspec:install
# spec/rails_helper.rb
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort("Running in production!") if Rails.env.production?
require 'rspec/rails'
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
# FactoryBot
config.include FactoryBot::Syntax::Methods
# Devise helpers (if using)
config.include Devise::Test::IntegrationHelpers, type: :request
end
# Shoulda Matchers
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
RSpec.describe Article, type: :model do
# described_class returns Article
subject { described_class.new(title: "Test", body: "Content") }
it { is_expected.to be_valid }
# Named subject for clarity
subject(:article) { described_class.new(title: "Test", body: "Content") }
it "has a title" do
expect(article.title).to eq("Test")
end
end
| Method | Behavior |
|---|---|
let | Lazy-loaded, memoized per example |
let! | Evaluated before each example (eager) |
RSpec.describe Article do
let(:user) { create(:user) } # Created only when called
let!(:article) { create(:article) } # Created before each example
it "uses let! for setup that must exist" do
# article already exists in DB
expect(Article.count).to eq(1)
end
end
RSpec.describe Article do
before(:all) { @expensive = ExpensiveSetup.new } # Once per group
before(:each) { @article = create(:article) } # Before each example
before { @article = create(:article) } # Same as :each
after(:each) { cleanup_files }
after(:all) { @expensive.teardown }
around(:each) do |example|
Timecop.freeze(Time.local(2024)) do
example.run
end
end
end
# spec/support/shared_examples/publishable.rb
RSpec.shared_examples "publishable" do
describe "#publish!" do
it "sets status to published" do
subject.publish!
expect(subject.status).to eq("published")
end
end
end
# Usage
RSpec.describe Article do
subject { create(:article) }
it_behaves_like "publishable"
end
# With parameters
RSpec.shared_examples "has status" do |valid_statuses|
it "validates status" do
valid_statuses.each do |status|
subject.status = status
expect(subject).to be_valid
end
end
end
it_behaves_like "has status", %w[draft published]
RSpec.describe Article do
it "does something" do
pending "waiting for API"
expect(article.sync).to be_truthy
end
xit "is skipped entirely" do
end
it "conditionally skips", skip: "CI issue" do
end
end
it "has valid attributes", :aggregate_failures do
user = create(:user)
expect(user.name).to be_present # All run even if one fails
expect(user.email).to include("@")
expect(user.role).to eq("member")
end
# spec/models/article_spec.rb
require 'rails_helper'
RSpec.describe Article, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:comments).dependent(:destroy) }
it { is_expected.to have_many(:tags).through(:taggings) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(255) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w[draft published]) }
end
describe 'scopes' do
describe '.published' do
it 'returns only published articles' do
published = create(:article, status: 'published')
draft = create(:article, status: 'draft')
expect(Article.published).to include(published)
expect(Article.published).not_to include(draft)
end
end
end
describe '#publish!' do
let(:article) { create(:article, status: 'draft') }
it 'changes status to published' do
expect { article.publish! }.to change { article.status }
.from('draft').to('published')
end
it 'sets published_at timestamp' do
freeze_time do
article.publish!
expect(article.published_at).to eq(Time.current)
end
end
end
end
# spec/requests/articles_spec.rb
require 'rails_helper'
RSpec.describe "Articles", type: :request do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
describe "GET /articles" do
before { create_list(:article, 3, status: 'published') }
it "returns published articles" do
get articles_path
expect(response).to have_http_status(:ok)
end
end
describe "POST /articles" do
context "when authenticated" do
before { sign_in user }
it "creates a new article" do
expect {
post articles_path, params: { article: attributes_for(:article) }
}.to change(Article, :count).by(1)
expect(response).to redirect_to(article_path(Article.last))
end
it "returns unprocessable_entity for invalid params" do
post articles_path, params: { article: { title: "" } }
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "when not authenticated" do
it "redirects to sign in" do
post articles_path, params: { article: attributes_for(:article) }
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe "DELETE /articles/:id" do
before { sign_in user }
it "deletes the article" do
article # create before expect
expect { delete article_path(article) }.to change(Article, :count).by(-1)
end
end
end
# spec/requests/api/v1/articles_spec.rb
RSpec.describe "API V1 Articles", type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe "GET /api/v1/articles" do
before { create_list(:article, 3) }
it "returns articles as JSON" do
get api_v1_articles_path, headers: headers
expect(response).to have_http_status(:ok)
expect(response.content_type).to include("application/json")
json = JSON.parse(response.body)
expect(json['data'].length).to eq(3)
end
end
describe "POST /api/v1/articles" do
let(:valid_params) { { article: { title: "Test", body: "Content" } } }
it "creates article and returns JSON" do
post api_v1_articles_path, params: valid_params, headers: headers, as: :json
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['data']['attributes']['title']).to eq("Test")
end
end
end
# spec/system/articles_spec.rb
require 'rails_helper'
RSpec.describe "Articles", type: :system do
let(:user) { create(:user) }
before { driven_by(:selenium_chrome_headless) }
describe "creating an article" do
before { sign_in user }
it "allows creating a new article" do
visit new_article_path
fill_in "Title", with: "My Article"
fill_in "Body", with: "Content here."
click_button "Create Article"
expect(page).to have_content("Article was successfully created")
expect(page).to have_content("My Article")
end
it "shows validation errors" do
visit new_article_path
click_button "Create Article"
expect(page).to have_content("Title can't be blank")
end
end
describe "with Turbo/Hotwire", js: true do
let!(:article) { create(:article, user: user) }
it "adds comment without page reload" do
sign_in user
visit article_path(article)
fill_in "comment_body", with: "Great article!"
click_button "Add Comment"
within "#comments" do
expect(page).to have_content("Great article!")
end
end
end
end
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { "password123" }
name { Faker::Name.name }
trait :admin do
role { "admin" }
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, user: user)
end
end
end
end
# spec/factories/articles.rb
FactoryBot.define do
factory :article do
title { Faker::Lorem.sentence }
body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
status { "draft" }
user
trait :published do
status { "published" }
published_at { Time.current }
end
end
end
# Usage
create(:user) # Basic
create(:user, :admin) # With trait
create(:user, :with_articles, articles_count: 5) # Transient
build(:article) # Not saved
attributes_for(:article) # Hash only
# Stubbing methods
allow(Article).to receive(:featured).and_return([article])
allow(user).to receive(:admin?).and_return(true)
# With arguments
allow(PaymentService).to receive(:charge)
.with(user, 100)
.and_return(OpenStruct.new(success?: true))
# Expecting calls
expect(NotificationService).to receive(:notify)
.with(user, "Published")
.once
# Spies
mailer = spy("mailer")
allow(UserMailer).to receive(:welcome).and_return(mailer)
# ... action ...
expect(mailer).to have_received(:deliver_later)
# WebMock for external APIs
stub_request(:get, "https://api.example.com/data")
.to_return(status: 200, body: { items: [] }.to_json)
# Equality
expect(result).to eq(expected) # ==
expect(result).to eql(expected) # eql?
expect(result).to equal(expected) # same object
expect(result).to be(expected) # same as equal
# Truthiness
expect(value).to be_truthy
expect(value).to be_falsy
expect(value).to be_nil
expect(value).to be true
expect(value).to be false
# Comparisons
expect(10).to be > 5
expect(10).to be_between(5, 15).inclusive
expect(result).to be_within(0.01).of(3.14)
# Collections
expect(array).to include(item)
expect(array).to contain_exactly(1, 2, 3)
expect(array).to match_array([3, 2, 1])
expect(array).to start_with(1, 2)
expect(array).to all(be_positive)
# Strings
expect(string).to match(/regex/)
expect(string).to include("substring")
expect(string).to start_with("prefix")
# Types
expect(obj).to be_a(Class)
expect(obj).to be_an_instance_of(Class)
expect(obj).to respond_to(:method)
# Changes
expect { action }.to change(object, :attr).from(old).to(new)
expect { action }.to change { object.attr }.by(1)
expect { action }.not_to change(object, :attr)
# Errors
expect { action }.to raise_error(ErrorClass)
expect { action }.to raise_error(ErrorClass, "message")
# Compound
expect(value).to be_positive.and be < 100
expect(value).to eq(5).or eq(10)
spec/
├── factories/
├── fixtures/files/ # Upload test files
├── models/
├── requests/
│ └── api/v1/
├── services/
├── system/
├── support/
│ ├── capybara.rb
│ ├── shared_examples/
│ └── helpers.rb
├── rails_helper.rb
└── spec_helper.rb
bundle exec rspec # All tests
bundle exec rspec spec/models # Directory
bundle exec rspec spec/models/user_spec.rb # Single file
bundle exec rspec spec/models/user_spec.rb:15 # Single test (line)
bundle exec rspec --tag focus # Tagged tests
bundle exec rspec --fail-fast # Stop on first failure
bundle exec rspec --only-failures # Re-run failures
bundle exec rspec --format documentation # Verbose output
references/rspec-patterns.md - Advanced patterns, custom matchers, shared contextsThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.