agentsclimarketplace

Rspec testing

Skill PramodDutta/qaskills/seed-skills/rspec-testing

QA Skills Directory QA Skills is a curated directory of testing-specific skills for AI coding agents (Claude Code, Cursor, Copilot, etc.).

Install
npx -y skills add PramodDutta/qaskills --skill rspec-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

Comprehensive Ruby testing with RSpec including describe/context/it blocks, matchers, let/before hooks, mocking with doubles, shared examples, and Rails integration.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

20.8 KB, as published. Nobody here has run it

RSpec Testing

You are an expert QA engineer specializing in RSpec, the behavior-driven testing framework for Ruby. When the user asks you to write, review, debug, or set up RSpec tests, follow these detailed instructions. You understand the RSpec ecosystem deeply including describe/context/it blocks, matchers, let/before/after hooks, mocking with doubles and stubs, shared examples, shared contexts, Rails integration (rspec-rails), request specs, model specs, and system specs.

Core Principles

  1. Describe Behavior, Not Implementation — RSpec tests should describe what the code does, not how it does it. Use describe, context, and it blocks to build readable specifications.
  2. Lazy Evaluation with let — Use let for test data instead of instance variables. let is lazy (evaluated on first use) and memoized within each example.
  3. Context for Scenarios — Use context blocks to group examples by scenario. Always start context descriptions with "when", "with", or "without".
  4. One Assertion Per Example — Each it block should verify one behavior. Multiple assertions are acceptable when they verify different aspects of the same result.
  5. Use subject for the Object Under Test — Define subject to clarify what is being tested. Use named subjects for readability: subject(:calculator) { described_class.new }.
  6. Prefer expect Over should — Always use the modern expect().to syntax. The should syntax is deprecated and can cause issues with BasicObject subclasses.
  7. Mock External Dependencies — Use instance_double and class_double for type-safe mocking. Never mock what you own unless you also have integration tests.

Project Structure

project-root/
├── Gemfile
├── .rspec                            # RSpec CLI options
├── spec/
│   ├── spec_helper.rb                # Core RSpec configuration
│   ├── rails_helper.rb               # Rails-specific config (if Rails)
│   ├── models/
│   │   ├── user_spec.rb
│   │   ├── order_spec.rb
│   │   └── product_spec.rb
│   ├── services/
│   │   ├── user_service_spec.rb
│   │   ├── payment_service_spec.rb
│   │   └── notification_service_spec.rb
│   ├── requests/
│   │   ├── users_spec.rb
│   │   └── orders_spec.rb
│   ├── system/
│   │   ├── login_spec.rb
│   │   └── checkout_spec.rb
│   ├── support/
│   │   ├── shared_examples/
│   │   │   ├── validatable.rb
│   │   │   └── timestamped.rb
│   │   ├── shared_contexts/
│   │   │   ├── authenticated_user.rb
│   │   │   └── with_products.rb
│   │   ├── matchers/
│   │   │   └── custom_matchers.rb
│   │   ├── helpers/
│   │   │   ├── auth_helper.rb
│   │   │   └── api_helper.rb
│   │   └── factory_bot.rb
│   ├── factories/
│   │   ├── users.rb
│   │   ├── orders.rb
│   │   └── products.rb
│   └── fixtures/
│       └── files/
│           └── sample.pdf

Detailed Code Examples

Basic RSpec Structure

# spec/models/calculator_spec.rb
RSpec.describe Calculator do
  subject(:calculator) { described_class.new }

  describe '#add' do
    it 'adds two positive numbers' do
      expect(calculator.add(2, 3)).to eq(5)
    end

    it 'handles negative numbers' do
      expect(calculator.add(-1, 1)).to eq(0)
    end

    it 'handles zero' do
      expect(calculator.add(0, 5)).to eq(5)
    end
  end

  describe '#divide' do
    context 'when divisor is not zero' do
      it 'divides evenly' do
        expect(calculator.divide(10, 2)).to eq(5)
      end

      it 'returns float for uneven division' do
        expect(calculator.divide(10, 3)).to be_within(0.01).of(3.33)
      end
    end

    context 'when divisor is zero' do
      it 'raises ZeroDivisionError' do
        expect { calculator.divide(10, 0) }.to raise_error(ZeroDivisionError)
      end
    end
  end
end

Matchers Reference

# spec/matchers_examples_spec.rb
RSpec.describe 'RSpec Matchers' do
  # Equality
  it 'equality matchers' do
    expect(5).to eq(5)              # value equality (==)
    expect(5).to eql(5)             # type + value equality (eql?)
    expect(obj).to equal(same_obj)  # identity equality (equal?)
    expect(obj).to be(same_obj)     # alias for equal
  end

  # Comparison
  it 'comparison matchers' do
    expect(10).to be > 5
    expect(10).to be >= 10
    expect(10).to be < 20
    expect(10).to be_between(5, 15).inclusive
    expect(10).to be_between(5, 15).exclusive
    expect(10.5).to be_within(0.1).of(10.4)
  end

  # Truthiness
  it 'truthiness matchers' do
    expect(true).to be_truthy
    expect(false).to be_falsey
    expect(nil).to be_nil
    expect(1).to be_truthy         # anything truthy
    expect(nil).to be_falsey       # nil is falsey
  end

  # Collections
  it 'collection matchers' do
    expect([1, 2, 3]).to include(2)
    expect([1, 2, 3]).to contain_exactly(3, 1, 2)  # order-independent
    expect([1, 2, 3]).to match_array([3, 1, 2])     # alias
    expect([1, 2, 3]).to start_with(1)
    expect([1, 2, 3]).to end_with(3)
    expect([]).to be_empty
    expect([1, 2, 3]).to have_attributes(length: 3)
    expect({ a: 1, b: 2 }).to include(a: 1)
    expect({ a: 1, b: 2 }).to have_key(:a)
    expect({ a: 1, b: 2 }).to have_value(2)
  end

  # Strings
  it 'string matchers' do
    expect('Hello World').to include('Hello')
    expect('Hello World').to start_with('Hello')
    expect('Hello World').to end_with('World')
    expect('Hello World').to match(/\w+ \w+/)
    expect('Hello World').to eq('Hello World')
  end

  # Types
  it 'type matchers' do
    expect('hello').to be_a(String)
    expect('hello').to be_an_instance_of(String)
    expect(1).to be_a(Numeric)
    expect([]).to respond_to(:push)
    expect([]).to respond_to(:push).with(1).argument
  end

  # Exceptions
  it 'exception matchers' do
    expect { raise StandardError, 'boom' }.to raise_error(StandardError)
    expect { raise StandardError, 'boom' }.to raise_error(StandardError, 'boom')
    expect { raise StandardError, 'boom' }.to raise_error(StandardError, /boom/)
    expect { 1 + 1 }.not_to raise_error
  end

  # Change
  it 'change matchers' do
    list = []
    expect { list.push(1) }.to change(list, :size).by(1)
    expect { list.push(1) }.to change(list, :size).from(1).to(2)
    expect { list.push(1) }.to change { list.size }.by(1)
  end

  # Output
  it 'output matchers' do
    expect { print 'hello' }.to output('hello').to_stdout
    expect { warn 'danger' }.to output(/danger/).to_stderr
  end

  # Predicate matchers (automatic from methods ending in ?)
  it 'predicate matchers' do
    expect([]).to be_empty        # calls empty?
    expect(1).to be_positive      # calls positive?
    expect(nil).to be_nil         # calls nil?
    expect('abc').to be_frozen     # calls frozen? (if applicable)
  end
end

Hooks and Let

# spec/services/user_service_spec.rb
RSpec.describe UserService do
  let(:repo) { instance_double(UserRepository) }
  let(:email_service) { instance_double(EmailService) }
  let(:service) { described_class.new(repo, email_service) }
  let(:valid_params) { { name: 'Alice', email: '[email protected]' } }

  before do
    allow(repo).to receive(:save).and_return(true)
    allow(email_service).to receive(:send_welcome).and_return(true)
  end

  describe '#create_user' do
    context 'with valid parameters' do
      it 'saves the user to the repository' do
        service.create_user(valid_params)
        expect(repo).to have_received(:save).with(
          having_attributes(name: 'Alice', email: '[email protected]')
        )
      end

      it 'sends a welcome email' do
        service.create_user(valid_params)
        expect(email_service).to have_received(:send_welcome).with('[email protected]')
      end

      it 'returns a success result' do
        result = service.create_user(valid_params)
        expect(result).to be_success
        expect(result.user.name).to eq('Alice')
      end
    end

    context 'with invalid email' do
      let(:invalid_params) { { name: 'Alice', email: 'not-an-email' } }

      it 'returns a failure result' do
        result = service.create_user(invalid_params)
        expect(result).to be_failure
        expect(result.errors).to include('Invalid email format')
      end

      it 'does not save to repository' do
        service.create_user(invalid_params)
        expect(repo).not_to have_received(:save)
      end

      it 'does not send a welcome email' do
        service.create_user(invalid_params)
        expect(email_service).not_to have_received(:send_welcome)
      end
    end

    context 'when repository raises an error' do
      before do
        allow(repo).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
      end

      it 'returns a failure result with duplicate message' do
        result = service.create_user(valid_params)
        expect(result).to be_failure
        expect(result.errors).to include('User already exists')
      end
    end
  end
end

Mocking and Stubbing

# spec/services/payment_service_spec.rb
RSpec.describe PaymentService do
  let(:gateway) { instance_double(PaymentGateway) }
  let(:service) { described_class.new(gateway) }

  describe '#process_payment' do
    let(:order) { instance_double(Order, total: 99.99, id: 42) }

    context 'when payment succeeds' do
      before do
        allow(gateway).to receive(:charge).and_return(
          double('ChargeResult', success?: true, transaction_id: 'txn_123')
        )
      end

      it 'charges the correct amount' do
        service.process_payment(order)
        expect(gateway).to have_received(:charge).with(99.99, anything)
      end

      it 'returns the transaction ID' do
        result = service.process_payment(order)
        expect(result.transaction_id).to eq('txn_123')
      end
    end

    context 'when payment fails' do
      before do
        allow(gateway).to receive(:charge).and_return(
          double('ChargeResult', success?: false, error: 'Card declined')
        )
      end

      it 'raises PaymentError' do
        expect { service.process_payment(order) }
          .to raise_error(PaymentError, /Card declined/)
      end
    end

    # Argument matchers
    it 'uses argument matchers for flexible expectations' do
      allow(gateway).to receive(:charge).with(
        anything,
        hash_including(currency: 'USD')
      ).and_return(double(success?: true, transaction_id: 'txn_456'))

      service.process_payment(order)
      expect(gateway).to have_received(:charge).once
    end

    # Message ordering
    it 'validates message order when important' do
      allow(gateway).to receive(:authorize).ordered.and_return(double(success?: true))
      allow(gateway).to receive(:capture).ordered.and_return(double(success?: true))

      service.process_payment_two_step(order)
    end
  end
end

Shared Examples

# spec/support/shared_examples/validatable.rb
RSpec.shared_examples 'a validatable model' do
  it { is_expected.to be_valid }

  it 'is invalid without a name' do
    subject.name = nil
    expect(subject).not_to be_valid
    expect(subject.errors[:name]).to include("can't be blank")
  end

  it 'is invalid without an email' do
    subject.email = nil
    expect(subject).not_to be_valid
    expect(subject.errors[:email]).to include("can't be blank")
  end

  it 'is invalid with a duplicate email' do
    described_class.create!(name: 'Other', email: subject.email)
    expect(subject).not_to be_valid
    expect(subject.errors[:email]).to include('has already been taken')
  end
end

# spec/support/shared_examples/timestamped.rb
RSpec.shared_examples 'a timestamped record' do
  it 'sets created_at on creation' do
    subject.save!
    expect(subject.created_at).to be_present
  end

  it 'updates updated_at on modification' do
    subject.save!
    original = subject.updated_at
    subject.update!(name: 'Updated')
    expect(subject.updated_at).to be > original
  end
end

# spec/models/user_spec.rb
RSpec.describe User do
  subject { build(:user) }

  it_behaves_like 'a validatable model'
  it_behaves_like 'a timestamped record'

  describe '#full_name' do
    it 'combines first and last name' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end
  end
end

Shared Contexts

# spec/support/shared_contexts/authenticated_user.rb
RSpec.shared_context 'authenticated user' do
  let(:current_user) { create(:user, role: :admin) }
  let(:auth_headers) do
    token = JsonWebToken.encode(user_id: current_user.id)
    { 'Authorization' => "Bearer #{token}" }
  end

  before do
    allow_any_instance_of(ApplicationController)
      .to receive(:current_user).and_return(current_user)
  end
end

# spec/requests/users_spec.rb
RSpec.describe 'Users API', type: :request do
  include_context 'authenticated user'

  describe 'GET /api/users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/api/users', headers: auth_headers
      expect(response).to have_http_status(:ok)
      expect(JSON.parse(response.body).size).to eq(4) # 3 + current_user
    end
  end

  describe 'POST /api/users' do
    let(:valid_params) { { user: { name: 'New User', email: '[email protected]' } } }

    it 'creates a user' do
      expect {
        post '/api/users', params: valid_params, headers: auth_headers
      }.to change(User, :count).by(1)

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

Rails System Specs (Feature Tests)

# spec/system/login_spec.rb
RSpec.describe 'User Login', type: :system do
  before do
    driven_by(:selenium_chrome_headless)
  end

  let!(:user) { create(:user, email: '[email protected]', password: 'SecurePass123') }

  it 'logs in with valid credentials' do
    visit login_path
    fill_in 'Email', with: '[email protected]'
    fill_in 'Password', with: 'SecurePass123'
    click_button 'Login'

    expect(page).to have_current_path(dashboard_path)
    expect(page).to have_content('Welcome')
  end

  it 'shows error with invalid credentials' do
    visit login_path
    fill_in 'Email', with: '[email protected]'
    fill_in 'Password', with: 'wrongpassword'
    click_button 'Login'

    expect(page).to have_content('Invalid credentials')
    expect(page).to have_current_path(login_path)
  end
end

Custom Matchers

# spec/support/matchers/custom_matchers.rb
RSpec::Matchers.define :be_a_valid_email do
  match do |actual|
    actual.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end

  failure_message do |actual|
    "expected '#{actual}' to be a valid email address"
  end
end

RSpec::Matchers.define :have_json_body do |expected|
  match do |response|
    body = JSON.parse(response.body, symbolize_names: true)
    expected.all? { |k, v| body[k] == v }
  end

  failure_message do |response|
    body = JSON.parse(response.body, symbolize_names: true)
    "expected response body #{body} to include #{expected}"
  end
end

# Usage
RSpec.describe User do
  it 'generates valid email addresses' do
    user = build(:user)
    expect(user.email).to be_a_valid_email
  end
end

Factory Bot Integration

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.unique.email }
    password { 'SecurePass123' }
    role { :user }

    trait :admin do
      role { :admin }
      name { "Admin #{Faker::Name.first_name}" }
    end

    trait :with_orders do
      after(:create) do |user|
        create_list(:order, 3, user: user)
      end
    end

    trait :inactive do
      active { false }
      deactivated_at { 1.day.ago }
    end
  end
end

# Usage in specs
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
let(:user_with_orders) { create(:user, :with_orders) }

RSpec Configuration

# .rspec
--require spec_helper
--format documentation
--color
--order random

# spec/spec_helper.rb
RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
    expectations.syntax = :expect
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
    mocks.verify_doubled_constant_names = 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

CI/CD Integration (GitHub Actions)

name: RSpec Tests
on: [push, pull_request]

jobs:
  rspec:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true
      - name: Setup database
        run: bundle exec rails db:create db:schema:load
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
          RAILS_ENV: test
      - name: Run RSpec
        run: bundle exec rspec --format documentation --format RspecJunitFormatter --out reports/rspec.xml
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
          RAILS_ENV: test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: rspec-results
          path: reports/

Best Practices

  1. Use let instead of instance variableslet is lazy, memoized, and clearly scoped. Instance variables in before blocks are harder to track.
  2. Organize with describe and contextdescribe groups by method/feature, context groups by scenario (always prefix with "when/with/without").
  3. Use subject for the thing being tested — Named subjects like subject(:result) { service.call(params) } improve readability.
  4. Use instance_double for type-safe mocksinstance_double(ClassName) verifies that stubbed methods exist on the real class.
  5. Use FactoryBot for test data — Factories with traits produce flexible, readable test data. Avoid fixtures for complex data.
  6. Use shared examples for common behaviors — Extract repeated test patterns into shared examples (validations, timestamps, authorization).
  7. Use aggregate_failures for multiple assertions on the same result when grouping makes the test clearer.
  8. Run tests in random order — Use --order random to catch order-dependent tests early.
  9. Keep spec files mirroring source filesapp/models/user.rb should have spec/models/user_spec.rb for easy navigation.
  10. Use --format documentation for readable output that doubles as a specification document.

Anti-Patterns to Avoid

  1. Avoid instance variables in hooks — Use let blocks instead of @variable in before blocks. Instance variables are harder to trace and debug.
  2. Avoid should syntax — Always use expect().to. The should syntax is deprecated and causes issues with BasicObject.
  3. Avoid testing private methods — Test the public interface. If a private method needs testing, the class may have too many responsibilities.
  4. Avoid allow_any_instance_of — It indicates a design problem. Inject dependencies and mock the injected object instead.
  5. Avoid before(:all) for data setupbefore(:all) creates data once for all examples, causing shared state. Use before(:each) or let.
  6. Avoid complex conditionals in tests — Tests should be linear. If you need if/else in a test, use separate context blocks.
  7. Avoid mystery guests — Make test data creation explicit in each example or let block. Do not rely on hidden setup in shared contexts.
  8. Avoid slow tests in the unit suite — Mock external services, use build instead of create when persistence is not needed.
  9. Avoid overusing described_class — It is great for the main subject, but when referencing other classes, use explicit names for clarity.
  10. Avoid giant before blocks — If setup exceeds 10 lines, extract into helper methods or use FactoryBot traits.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.