rspec

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

RSpec Testing Skill

RSpec测试技能指南

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.
为Ruby和Rails应用编写全面RSpec测试的专业指南。本技能提供即时、可落地的测试策略,以及针对复杂场景的深度参考内容。

Quick Start

快速入门

Basic RSpec Structure

RSpec基础结构

ruby
undefined
ruby
undefined

spec/models/user_spec.rb

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

**核心概念:**

- `describe`:对相关测试进行分组(类、方法)
- `context`:描述特定场景
- `it`:单个测试用例
- `expect`:使用匹配器进行断言

Running Tests

运行测试

bash
undefined
bash
undefined

Run all specs

运行所有测试

bundle exec rspec
bundle exec rspec

Run specific file

运行指定文件

bundle exec rspec spec/models/user_spec.rb
bundle exec rspec spec/models/user_spec.rb

Run specific line

运行指定行的测试

bundle exec rspec spec/models/user_spec.rb:12
bundle exec rspec spec/models/user_spec.rb:12

Run with documentation format

以文档格式输出结果

bundle exec rspec --format documentation
bundle exec rspec --format documentation

Run only failures from last run

仅运行上次失败的测试

bundle exec rspec --only-failures
undefined
bundle exec rspec --only-failures
undefined

Core Testing Patterns

核心测试模式

1. Model Specs

1. 模型测试

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
测试业务逻辑、验证规则、关联关系和方法:
ruby
RSpec.describe Article, type: :model do
  # 测试验证规则
  describe 'validations' do
    it { should validate_presence_of(:title) }
    it { should validate_length_of(:title).is_at_most(100) }
  end

  # 测试关联关系
  describe 'associations' do
    it { should belong_to(:author) }
    it { should have_many(:comments) }
  end

  # 测试实例方法
  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

  # 测试作用域
  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

2. 请求测试

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
测试整个技术栈中的HTTP请求与响应:
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)

3. 系统测试(端到端)

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
使用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

  # 测试JavaScript交互
  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

Factory Bot集成

Defining Factories

定义工厂

ruby
undefined
ruby
undefined

spec/factories/users.rb

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
FactoryBot.define do factory :user do first_name { 'John' } last_name { 'Doe' } sequence(:email) { |n| "user#{n}@example.com" } password { 'password123' }
# 用于变体的特征
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
undefined
user = create(:user) # 已持久化 user = build(:user) # 未持久化 admin = create(:user, :admin) # 使用特征 user = create(:user, :with_articles) # 带关联 users = create_list(:user, 5) # 多条记录 attributes = attributes_for(:user) # 属性哈希
undefined

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
ruby
expect(actual).to eq(expected)           # ==
expect(actual).to eql(expected)          # .eql?
expect(actual).to be(expected)           # .equal?
expect(actual).to equal(expected)        # 同一对象

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)
ruby
expect(actual).to be_truthy              # 非nil或false
expect(actual).to be_falsy               # nil或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)
ruby
expect(array).to include(item)
expect(array).to contain_exactly(1, 2, 3)   # 任意顺序
expect(array).to match_array([1, 2, 3])     # 任意顺序
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')
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

Rails专属

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)
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
undefined
ruby
undefined

Basic double

基础替身

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

Verifying double (checks against real class)

验证式替身(检查真实类)

book = instance_double('Book', title: 'RSpec Book')
undefined
book = instance_double('Book', title: 'RSpec Book')
undefined

Stubbing Methods

方法存根

ruby
undefined
ruby
undefined

On test doubles

在测试替身上

allow(book).to receive(:title).and_return('New Title') allow(book).to receive(:available?).and_return(true)
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)
user = User.new allow(user).to receive(:admin?).and_return(true)

Chaining

链式调用

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

Message Expectations

消息期望

ruby
undefined
ruby
undefined

Expect method to be called

期望方法被调用

expect(mailer).to receive(:deliver).and_return(true)
expect(mailer).to receive(:deliver).and_return(true)

With specific arguments

带特定参数

expect(service).to receive(:call).with(user, { notify: true })
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)
undefined
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)
undefined

Spies

间谍

ruby
undefined
ruby
undefined

Create spy

创建间谍

invitation = spy('invitation') user.accept_invitation(invitation)
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)
undefined
expect(invitation).to have_received(:accept) expect(invitation).to have_received(:accept).with(mailer)
undefined

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
ruby
RSpec.describe ArticlesController do
  before(:each) do
    @user = create(:user)
    sign_in @user
  end

  # 或使用subject
  subject { create(:article) }

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

Let and Let

Let与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
ruby
describe Article do
  let(:article) { create(:article) }           # 懒加载
  let!(:published) { create(:article, :published) }  # 预加载

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

Shared Examples

共享示例

ruby
undefined
ruby
undefined

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
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
undefined
describe Article do it_behaves_like 'a timestamped model' end
describe Comment do it_behaves_like 'a timestamped model' end
undefined

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

  # 测试使用current_user并已登录
end

TDD Workflow

TDD工作流

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
  1. :先编写失败的测试
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'

失败:undefined method `full_name'


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

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

2. **绿**:编写最少代码使测试通过

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

Passes!

通过!


3. **Refactor**: Improve code while keeping tests green

3. **重构**:在保持测试通过的同时优化代码

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
从系统测试开始针对用户可见功能:
  • 测试完整工作流
  • 可信度最高
  • 运行速度最慢
使用请求测试针对API/控制器逻辑:
  • 测试HTTP交互
  • 比系统测试快
  • 覆盖认证、授权、边界场景
使用模型测试针对业务逻辑:
  • 测试计算、验证、作用域
  • 快速且聚焦
  • 占测试套件的大部分

Configuration Best Practices

配置最佳实践

spec/rails_helper.rb

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'
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
undefined
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
RSpec.configure do |config|

使用事务性 fixtures

config.use_transactional_fixtures = true

从文件位置推断测试类型

config.infer_spec_type_from_file_location!

过滤Rails调用栈

config.filter_rails_from_backtrace!

包含FactoryBot方法

config.include FactoryBot::Syntax::Methods

包含请求助手

config.include RequestHelpers, type: :request

系统测试的Capybara配置

config.before(:each, type: :system) do driven_by :selenium_chrome_headless end end
undefined

spec/spec_helper.rb

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
ruby
RSpec.configure do |config|
  # 保存测试状态以便后续查看
  config.example_status_persistence_file_path = "spec/examples.txt"

  # 禁用猴子补丁(仅使用expect语法)
  config.disable_monkey_patching!

  # 输出警告
  config.warnings = true

  # 分析最慢的测试
  config.profile_examples = 10 if ENV['PROFILE']

  # 随机顺序运行测试
  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
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(['noreply@example.com'])
    end

    it 'contains the user name' do
      expect(mail.body.encoded).to include(user.name)
    end
  end
end
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(['noreply@example.com'])
    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
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
  1. 使用let而非before实现懒加载
  2. 测试逻辑时避免数据库调用(使用模拟对象)
  3. 无需持久化时使用build而非create
  4. 使用build_stubbed创建带关联的非持久化对象
  5. 标记慢测试并在开发中排除:
    ruby
    it 'slow test', :slow do
      # 测试代码
    end
    
    # 运行命令: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)
  • 模型测试:业务逻辑、计算、验证、作用域
  • 请求测试:API端点、认证、授权、JSON响应
  • 系统测试:用户工作流、JavaScript交互、表单提交
  • 邮件测试:邮件内容、收件人、附件
  • 任务测试:后台任务的入队与执行
  • 助手测试:视图助手方法
  • 路由测试:自定义路由(通常不需要)

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

最常用命令:
bash
rspec                          # 运行所有测试
rspec spec/models              # 运行模型测试
rspec --tag ~slow              # 排除慢测试
rspec --only-failures          # 重跑失败测试
rspec --format documentation   # 可读格式输出
rspec --profile               # 显示最慢的测试
最常用匹配器:
  • eq(expected)
    - 值相等
  • be_truthy
    /
    be_falsy
    - 真值判断
  • include(item)
    - 集合成员
  • raise_error(Error)
    - 异常捕获
  • change { }.by(n)
    - 状态变更
最常用存根:
  • allow(obj).to receive(:method)
    - 存根方法
  • expect(obj).to receive(:method)
    - 期望调用
  • double('name', method: value)
    - 创建替身

Reference Documentation

参考文档

For detailed information on specific topics, see the references directory:
  • Core Concepts - Describe blocks, contexts, hooks, subject, let
  • Matchers Guide - Complete matcher reference with examples
  • Mocking and Stubbing - Test doubles, stubs, spies, message expectations
  • Rails Testing - Rails-specific spec types and helpers
  • Factory Bot - Test data strategies and patterns
  • Best Practices - Testing philosophy, patterns, and anti-patterns
  • Configuration - Setup, formatters, and optimization
如需特定主题的详细信息,请查看参考目录:
  • 核心概念 - 描述块、上下文、钩子、subject、let
  • 匹配器指南 - 完整匹配器参考及示例
  • 模拟与存根 - 测试替身、存根、间谍、消息期望
  • Rails测试 - Rails专属测试类型与助手
  • Factory Bot - 测试数据策略与模式
  • 最佳实践 - 测试理念、模式与反模式
  • 配置 - 设置、格式化工具与优化

Common Scenarios

常见场景

Debugging Failing Tests

调试失败的测试

ruby
undefined
ruby
undefined

Use save_and_open_page in system specs

在系统测试中使用save_and_open_page

scenario 'user creates article' do visit new_article_path save_and_open_page # Opens browser with current page state

...

end
scenario 'user creates article' do visit new_article_path save_and_open_page # 在浏览器中打开当前页面状态

...

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
it 'creates article' do post '/articles', params: { ... } puts response.body # 调试API响应 expect(response).to be_successful end

Use binding.pry for interactive debugging

使用binding.pry进行交互式调试

it 'calculates total' do order = create(:order) binding.pry # Pause execution here expect(order.total).to eq(100) end
undefined
it 'calculates total' do order = create(:order) binding.pry # 在此处暂停执行 expect(order.total).to eq(100) end
undefined

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
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: '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.
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: 'USER@EXAMPLE.COM')
      expect(user.email).to eq('user@example.com')
    end
  end
end
本技能提供全面的RSpec测试指南。如需特定场景或高级技术,请参考
references/
目录下的详细参考文档。