Claude Code Plugins

Community-maintained marketplace

Feedback

Comprehensive RSpec testing for Ruby and Rails applications. Covers model specs, request specs, system specs, factories, mocks, and TDD workflow. Automatically triggers on RSpec-related keywords and testing scenarios.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rspec
description Comprehensive RSpec testing for Ruby and Rails applications. Covers model specs, request specs, system specs, factories, mocks, and TDD workflow. Automatically triggers on RSpec-related keywords and testing scenarios.

RSpec Testing Skill

Expert guidance for writing comprehensive tests in RSpec for Ruby and Rails applications. This skill provides immediate, actionable testing strategies with deep-dive references for complex scenarios.

Quick Start

Basic RSpec Structure

# spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe '#full_name' do
    it 'returns the first and last name' do
      user = User.new(first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end
  end
end

Key concepts:

  • describe: Groups related tests (classes, methods)
  • context: Describes specific scenarios
  • it: Individual test example
  • expect: Makes assertions using matchers

Running Tests

# Run all specs
bundle exec rspec

# Run specific file
bundle exec rspec spec/models/user_spec.rb

# Run specific line
bundle exec rspec spec/models/user_spec.rb:12

# Run with documentation format
bundle exec rspec --format documentation

# Run only failures from last run
bundle exec rspec --only-failures

Core Testing Patterns

1. Model Specs

Test business logic, validations, associations, and methods:

RSpec.describe Article, type: :model do
  # Test validations
  describe 'validations' do
    it { should validate_presence_of(:title) }
    it { should validate_length_of(:title).is_at_most(100) }
  end

  # Test associations
  describe 'associations' do
    it { should belong_to(:author) }
    it { should have_many(:comments) }
  end

  # Test instance methods
  describe '#published?' do
    context 'when publish_date is in the past' do
      it 'returns true' do
        article = Article.new(publish_date: 1.day.ago)
        expect(article.published?).to be true
      end
    end

    context 'when publish_date is in the future' do
      it 'returns false' do
        article = Article.new(publish_date: 1.day.from_now)
        expect(article.published?).to be false
      end
    end
  end

  # Test scopes
  describe '.recent' do
    it 'returns articles from the last 30 days' do
      old = create(:article, created_at: 31.days.ago)
      recent = create(:article, created_at: 1.day.ago)

      expect(Article.recent).to include(recent)
      expect(Article.recent).not_to include(old)
    end
  end
end

2. Request Specs

Test HTTP requests and responses across the entire stack:

RSpec.describe 'Articles API', type: :request do
  describe 'GET /articles' do
    it 'returns all articles' do
      create_list(:article, 3)

      get '/articles'

      expect(response).to have_http_status(:success)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /articles' do
    context 'with valid params' do
      it 'creates a new article' do
        article_params = { article: { title: 'New Article', body: 'Content' } }

        expect {
          post '/articles', params: article_params
        }.to change(Article, :count).by(1)

        expect(response).to have_http_status(:created)
      end
    end

    context 'with invalid params' do
      it 'returns errors' do
        invalid_params = { article: { title: '' } }

        post '/articles', params: invalid_params

        expect(response).to have_http_status(:unprocessable_entity)
      end
    end
  end

  describe 'authentication' do
    it 'requires authentication for create' do
      post '/articles', params: { article: { title: 'Test' } }

      expect(response).to have_http_status(:unauthorized)
    end

    it 'allows authenticated users to create' do
      user = create(:user)

      post '/articles',
        params: { article: { title: 'Test' } },
        headers: { 'Authorization' => "Bearer #{user.token}" }

      expect(response).to have_http_status(:created)
    end
  end
end

3. System Specs (End-to-End)

Test user workflows through the browser with Capybara:

RSpec.describe 'Article management', type: :system do
  before { driven_by(:selenium_chrome_headless) }

  scenario 'user creates an article' do
    visit new_article_path

    fill_in 'Title', with: 'My Article'
    fill_in 'Body', with: 'Article content'
    click_button 'Create Article'

    expect(page).to have_content('Article was successfully created')
    expect(page).to have_content('My Article')
  end

  scenario 'user edits an article' do
    article = create(:article, title: 'Original Title')

    visit article_path(article)
    click_link 'Edit'

    fill_in 'Title', with: 'Updated Title'
    click_button 'Update Article'

    expect(page).to have_content('Updated Title')
    expect(page).not_to have_content('Original Title')
  end

  # Test JavaScript interactions
  scenario 'user filters articles', js: true do
    create(:article, title: 'Ruby Article', category: 'ruby')
    create(:article, title: 'Python Article', category: 'python')

    visit articles_path

    select 'Ruby', from: 'filter'

    expect(page).to have_content('Ruby Article')
    expect(page).not_to have_content('Python Article')
  end
end

Factory Bot Integration

Defining Factories

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    first_name { 'John' }
    last_name { 'Doe' }
    sequence(:email) { |n| "user#{n}@example.com" }
    password { 'password123' }

    # Traits for variations
    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, author: user)
      end
    end
  end

  factory :article do
    sequence(:title) { |n| "Article #{n}" }
    body { 'Article content' }
    association :author, factory: :user
  end
end

# Using factories
user = create(:user)                        # Persisted
user = build(:user)                         # Not persisted
admin = create(:user, :admin)               # With trait
user = create(:user, :with_articles)        # With association
users = create_list(:user, 5)               # Multiple records
attributes = attributes_for(:user)          # Hash of attributes

Essential Matchers

Equality and Identity

expect(actual).to eq(expected)           # ==
expect(actual).to eql(expected)          # .eql?
expect(actual).to be(expected)           # .equal?
expect(actual).to equal(expected)        # same object

Truthiness and Types

expect(actual).to be_truthy              # not nil or false
expect(actual).to be_falsy               # nil or false
expect(actual).to be_nil
expect(actual).to be_a(Class)
expect(actual).to be_an_instance_of(Class)

Collections

expect(array).to include(item)
expect(array).to contain_exactly(1, 2, 3)   # any order
expect(array).to match_array([1, 2, 3])     # any order
expect(array).to start_with(1, 2)
expect(array).to end_with(2, 3)

Errors and Changes

expect { action }.to raise_error(ErrorClass)
expect { action }.to raise_error('message')
expect { action }.to change(User, :count).by(1)
expect { action }.to change { user.reload.name }.from('old').to('new')

Rails-Specific

expect(response).to have_http_status(:success)
expect(response).to have_http_status(200)
expect(response).to redirect_to(path)
expect { action }.to have_enqueued_job(JobClass)

Mocks, Stubs, and Doubles

Test Doubles

# Basic double
book = double('book', title: 'RSpec Book', pages: 300)

# Verifying double (checks against real class)
book = instance_double('Book', title: 'RSpec Book')

Stubbing Methods

# On test doubles
allow(book).to receive(:title).and_return('New Title')
allow(book).to receive(:available?).and_return(true)

# On real objects
user = User.new
allow(user).to receive(:admin?).and_return(true)

# Chaining
allow(user).to receive_message_chain(:articles, :published).and_return([article])

Message Expectations

# Expect method to be called
expect(mailer).to receive(:deliver).and_return(true)

# With specific arguments
expect(service).to receive(:call).with(user, { notify: true })

# Number of times
expect(logger).to receive(:info).once
expect(logger).to receive(:info).twice
expect(logger).to receive(:info).exactly(3).times
expect(logger).to receive(:info).at_least(:once)

Spies

# Create spy
invitation = spy('invitation')
user.accept_invitation(invitation)

# Verify after the fact
expect(invitation).to have_received(:accept)
expect(invitation).to have_received(:accept).with(mailer)

DRY Testing Techniques

Before Hooks

RSpec.describe ArticlesController do
  before(:each) do
    @user = create(:user)
    sign_in @user
  end

  # OR using subject
  subject { create(:article) }

  it 'has a title' do
    expect(subject.title).to be_present
  end
end

Let and Let

describe Article do
  let(:article) { create(:article) }           # Lazy-loaded
  let!(:published) { create(:article, :published) }  # Eager-loaded

  it 'can access article' do
    expect(article).to be_valid
  end
end

Shared Examples

# Define shared examples
RSpec.shared_examples 'a timestamped model' do
  it 'has created_at' do
    expect(subject).to respond_to(:created_at)
  end

  it 'has updated_at' do
    expect(subject).to respond_to(:updated_at)
  end
end

# Use shared examples
describe Article do
  it_behaves_like 'a timestamped model'
end

describe Comment do
  it_behaves_like 'a timestamped model'
end

Shared Contexts

RSpec.shared_context 'authenticated user' do
  let(:current_user) { create(:user) }

  before do
    sign_in current_user
  end
end

describe ArticlesController do
  include_context 'authenticated user'

  # Tests use current_user and are signed in
end

TDD Workflow

Red-Green-Refactor Cycle

  1. Red: Write a failing test first
describe User do
  it 'has a full name' do
    user = User.new(first_name: 'John', last_name: 'Doe')
    expect(user.full_name).to eq('John Doe')
  end
end
# Fails: undefined method `full_name'
  1. Green: Write minimal code to pass
class User
  def full_name
    "#{first_name} #{last_name}"
  end
end
# Passes!
  1. Refactor: Improve code while keeping tests green

Testing Strategy

Start with system specs for user-facing features:

  • Tests complete workflows
  • Highest confidence
  • Slowest to run

Drop to request specs for API/controller logic:

  • Test HTTP interactions
  • Faster than system specs
  • Cover authentication, authorization, edge cases

Use model specs for business logic:

  • Test calculations, validations, scopes
  • Fast and focused
  • Most of your test suite

Configuration Best Practices

spec/rails_helper.rb

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
abort("Run in production!") if Rails.env.production?
require 'rspec/rails'

# Auto-require support files
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

RSpec.configure do |config|
  # Use transactional fixtures
  config.use_transactional_fixtures = true

  # Infer spec type from file location
  config.infer_spec_type_from_file_location!

  # Filter Rails backtrace
  config.filter_rails_from_backtrace!

  # Include FactoryBot methods
  config.include FactoryBot::Syntax::Methods

  # Include request helpers
  config.include RequestHelpers, type: :request

  # Capybara configuration for system specs
  config.before(:each, type: :system) do
    driven_by :selenium_chrome_headless
  end
end

spec/spec_helper.rb

RSpec.configure do |config|
  # Show detailed failure messages
  config.example_status_persistence_file_path = "spec/examples.txt"

  # Disable monkey patching (use expect syntax only)
  config.disable_monkey_patching!

  # Output warnings
  config.warnings = true

  # Profile slowest tests
  config.profile_examples = 10 if ENV['PROFILE']

  # Run specs in random order
  config.order = :random
  Kernel.srand config.seed
end

Common Patterns

Testing Background Jobs

describe 'background jobs', type: :job do
  it 'enqueues the job' do
    expect {
      SendEmailJob.perform_later(user)
    }.to have_enqueued_job(SendEmailJob).with(user)
  end

  it 'performs the job' do
    expect {
      SendEmailJob.perform_now(user)
    }.to change { ActionMailer::Base.deliveries.count }.by(1)
  end
end

Testing Mailers

describe UserMailer, type: :mailer do
  describe '#welcome_email' do
    let(:user) { create(:user) }
    let(:mail) { UserMailer.welcome_email(user) }

    it 'renders the subject' do
      expect(mail.subject).to eq('Welcome!')
    end

    it 'renders the receiver email' do
      expect(mail.to).to eq([user.email])
    end

    it 'renders the sender email' do
      expect(mail.from).to eq(['noreply@example.com'])
    end

    it 'contains the user name' do
      expect(mail.body.encoded).to include(user.name)
    end
  end
end

Testing File Uploads

describe 'file upload', type: :system do
  it 'allows user to upload avatar' do
    user = create(:user)
    sign_in user

    visit edit_profile_path
    attach_file 'Avatar', Rails.root.join('spec', 'fixtures', 'avatar.jpg')
    click_button 'Update Profile'

    expect(page).to have_content('Profile updated')
    expect(user.reload.avatar).to be_attached
  end
end

Performance Tips

  1. Use let instead of before for lazy loading

  2. Avoid database calls when testing logic (use mocks)

  3. Use build instead of create when persistence isn't needed

  4. Use build_stubbed for non-persisted objects with associations

  5. Tag slow tests and exclude them during development:

    it 'slow test', :slow do
      # test code
    end
    
    # Run with: rspec --tag ~slow
    

When to Use Each Spec Type

  • Model specs: Business logic, calculations, validations, scopes
  • Request specs: API endpoints, authentication, authorization, JSON responses
  • System specs: User workflows, JavaScript interactions, form submissions
  • Mailer specs: Email content, recipients, attachments
  • Job specs: Background job enqueueing and execution
  • Helper specs: View helper methods
  • Routing specs: Custom routes (usually not needed)

Quick Reference

Most Common Commands:

rspec                          # Run all specs
rspec spec/models              # Run model specs
rspec --tag ~slow              # Exclude slow specs
rspec --only-failures          # Rerun failures
rspec --format documentation   # Readable output
rspec --profile               # Show slowest specs

Most Common Matchers:

  • eq(expected) - value equality
  • be_truthy / be_falsy - truthiness
  • include(item) - collection membership
  • raise_error(Error) - exceptions
  • change { }.by(n) - state changes

Most Common Stubs:

  • allow(obj).to receive(:method) - stub method
  • expect(obj).to receive(:method) - expect call
  • double('name', method: value) - create double

Reference Documentation

For detailed information on specific topics, see the references directory:

Common Scenarios

Debugging Failing Tests

# Use save_and_open_page in system specs
scenario 'user creates article' do
  visit new_article_path
  save_and_open_page  # Opens browser with current page state
  # ...
end

# Print response body in request specs
it 'creates article' do
  post '/articles', params: { ... }
  puts response.body  # Debug API responses
  expect(response).to be_successful
end

# Use binding.pry for interactive debugging
it 'calculates total' do
  order = create(:order)
  binding.pry  # Pause execution here
  expect(order.total).to eq(100)
end

Testing Complex Queries

describe '.search' do
  let!(:ruby_article) { create(:article, title: 'Ruby Guide', body: 'Ruby content') }
  let!(:rails_article) { create(:article, title: 'Rails Guide', body: 'Rails content') }

  it 'finds articles by title' do
    results = Article.search('Ruby')
    expect(results).to include(ruby_article)
    expect(results).not_to include(rails_article)
  end

  it 'finds articles by body' do
    results = Article.search('Rails content')
    expect(results).to include(rails_article)
  end
end

Testing Callbacks

describe 'callbacks' do
  describe 'after_create' do
    it 'sends welcome email' do
      expect(UserMailer).to receive(:welcome_email)
        .with(an_instance_of(User))
        .and_return(double(deliver_later: true))

      create(:user)
    end
  end

  describe 'before_save' do
    it 'normalizes email' do
      user = create(:user, email: 'USER@EXAMPLE.COM')
      expect(user.email).to eq('user@example.com')
    end
  end
end

This skill provides comprehensive RSpec testing guidance. For specific scenarios or advanced techniques, refer to the detailed reference documentation in the references/ directory.