home / skills / el-feo / ai-context / rspec

rspec skill

/plugins/ruby-rails/skills/rspec

npx playbooks add skill el-feo/ai-context --skill rspec

Review the files below or copy the command above to add this skill to your agents.

Files (8)
SKILL.md
17.5 KB
---
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

```ruby
# 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

```bash
# 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:

```ruby
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:

```ruby
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:

```ruby
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

```ruby
# 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

```ruby
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

```ruby
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

```ruby
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

```ruby
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

```ruby
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

```ruby
# 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

```ruby
# 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

```ruby
# 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

```ruby
# 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

```ruby
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

```ruby
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

```ruby
# 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

```ruby
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

```ruby
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'
```

2. **Green**: Write minimal code to pass

```ruby
class User
  def full_name
    "#{first_name} #{last_name}"
  end
end
# Passes!
```

3. **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

```ruby
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

```ruby
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

```ruby
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

```ruby
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(['[email protected]'])
    end

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

### Testing File Uploads

```ruby
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:

   ```ruby
   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:**

```bash
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:

- **[Core Concepts](./references/core_concepts.md)** - Describe blocks, contexts, hooks, subject, let
- **[Matchers Guide](./references/matchers.md)** - Complete matcher reference with examples
- **[Mocking and Stubbing](./references/mocking.md)** - Test doubles, stubs, spies, message expectations
- **[Rails Testing](./references/rails_testing.md)** - Rails-specific spec types and helpers
- **[Factory Bot](./references/factory_bot.md)** - Test data strategies and patterns
- **[Best Practices](./references/best_practices.md)** - Testing philosophy, patterns, and anti-patterns
- **[Configuration](./references/configuration.md)** - Setup, formatters, and optimization

## Common Scenarios

### Debugging Failing Tests

```ruby
# 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

```ruby
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

```ruby
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: '[email protected]')
      expect(user.email).to eq('[email protected]')
    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.