RSpec Rails 示例:构建坚如磐石的Rails应用测试体系
引言:为什么测试是Rails开发的生命线
你是否曾经历过这样的噩梦:刚修复一个bug,却在上线后发现它引发了三个新问题?或者花费数小时调试,最终发现只是一个简单的参数验证错误?在Rails开发中,缺乏完善测试体系的应用就像在雷区中行走——你永远不知道下一步会触发什么灾难。
RSpec-Rails-Examples项目(仓库地址:https://gitcode.com/gh_mirrors/rs/rspec-rails-examples)为我们提供了一个全面的测试范例集合,展示了如何使用RSpec(Ruby Spec,一种行为驱动开发框架)构建健壮的Rails应用测试。本文将带你深入探索这个宝藏项目,掌握从单元测试到集成测试的全栈测试技巧,让你的Rails应用在任何变更面前都能稳如泰山。
读完本文,你将获得:
- 一套完整的Rails测试策略,覆盖模型、控制器、API和用户界面
- 15+实用测试模式,解决90%的常见测试场景
- 7个核心测试工具的实战配置与应用
- 5类自定义匹配器,让测试代码更具可读性
- 一份可直接复用的测试模板,加速你的测试编写流程
RSpec测试金字塔:构建多层次测试防御体系
在深入具体测试示例之前,我们首先需要理解Rails应用的测试金字塔结构。一个健康的测试体系应该像埃及金字塔一样,拥有坚实的基础和合理的层次比例:
单元测试:验证独立组件的正确性
单元测试是测试金字塔的基石,专注于验证应用中最小可测试单元的行为。在Rails中,这通常意味着对模型(Model)的测试。让我们通过Subscription模型的测试示例,看看如何构建可靠的单元测试。
模型测试示例:Subscription模型
Subscription模型负责管理用户订阅信息,我们需要验证其数据验证、关联关系和业务逻辑。以下是一个完整的模型测试示例:
require 'rails_helper'
RSpec.describe Subscription, :type => :model do
# 数据库结构测试
context "db" do
context "indexes" do
it { should have_db_index(:email).unique(true) }
it { should have_db_index(:confirmation_token).unique(true) }
end
context "columns" do
it { should have_db_column(:email).of_type(:string).with_options(limit: 100, null: false) }
it { should have_db_column(:confirmation_token).of_type(:string).with_options(limit: 100, null: false) }
it { should have_db_column(:confirmed).of_type(:boolean).with_options(default: false, null: false) }
it { should have_db_column(:start_on).of_type(:date) }
end
end
# 属性测试
context "attributes" do
it "has email" do
expect(build(:subscription, email: "x@y.z")).to have_attributes(email: "x@y.z")
end
it "has confirmed" do
expect(build(:subscription, confirmed: true)).to have_attributes(confirmed: true)
end
# 时间相关测试
context "start_on" do
it "defaults to today" do
now = Time.zone.now
today = now.to_date
travel_to now do # 时间旅行测试
expect(build(:subscription).start_on).to eq(today)
end
end
end
end
# 验证测试
context "validation" do
let(:subscription) { build(:subscription, confirmation_token: "token", email: "a@b.c") }
it "requires unique email" do
expect(subscription).to validate_uniqueness_of(:email)
end
it "requires email" do
expect(subscription).to validate_presence_of(:email)
end
end
# 作用域测试
context "scopes" do
describe ".confirmation_overdue" do
before { travel_to Time.zone.now } # 冻结时间
after { travel_back } # 恢复时间
it "returns unconfirmed subscriptions older than 3 days" do
overdue = create(:subscription, confirmed: false, created_at: (3.days + 1.second).ago)
expect(Subscription.confirmation_overdue).to match_array [overdue]
end
it "does not return recent unconfirmed subscriptions" do
create(:subscription, confirmed: false, created_at: 3.days.ago)
expect(Subscription.confirmation_overdue).to be_empty
end
end
end
# 方法测试
describe "#to_param" do
it "uses confirmation_token as the default identifier for routes" do
subscription = build(:subscription, confirmation_token: "hello-im-a-token-123")
expect(subscription.to_param).to eq("hello-im-a-token-123")
end
end
describe ".create_and_request_confirmation(params)" do
it "creates an unconfirmed subscription with secure token" do
expect(::SecureRandom).to receive(:hex).with(32).and_call_original
subscription = Subscription.create_and_request_confirmation(email: "hello@example.tld")
subscription.reload
expect(subscription.confirmed?).to eq(false)
expect(subscription.confirmation_token).to match(/\A[a-z0-9]{64}\z/)
end
it "emails a confirmation request" do
expect(SubscriptionMailer)
.to receive(:send_confirmation_request!)
.with(Subscription)
Subscription.create_and_request_confirmation(email: "subscriber@somedomain.tld")
end
it "doesn't create subscription if emailing fails" do
expect(SubscriptionMailer)
.to receive(:send_confirmation_request!)
.and_raise("Delivery failed!")
expect {
Subscription.create_and_request_confirmation(email: "subscriber@somedomain.tld")
}.to raise_error "Delivery failed!"
expect(Subscription.exists?).to eq(false)
end
end
end
这个测试示例展示了模型测试的多个维度,包括:
- 数据库结构测试:验证数据库索引和字段定义
- 属性测试:确保模型属性正确初始化和默认值设置
- 验证测试:检查数据验证规则
- 作用域测试:测试模型查询作用域
- 方法测试:验证模型实例方法和类方法的行为
特别值得注意的是时间相关测试中使用的travel_to方法,这是Rails提供的时间旅行工具,能够冻结或修改测试环境中的当前时间,确保时间敏感代码的测试可靠性。
集成测试:验证组件协作的正确性
集成测试关注不同组件之间的交互,在Rails应用中主要表现为控制器(Controller)测试和API测试。这类测试确保系统各部分能够协同工作,处理请求并返回正确响应。
控制器测试示例:SubscriptionsController
SubscriptionsController处理订阅相关的HTTP请求,我们需要测试其各种动作的行为:
require 'rails_helper'
RSpec.describe SubscriptionsController, :type => :controller do
context "GET new" do
it "assigns a blank subscription to the view" do
get :new
expect(assigns(:subscription)).to be_a_new(Subscription)
end
end
context "POST create" do
it "redirects to pending subscriptions page" do
params = { subscription: { email: "e@example.tld", start_on: "2014-12-31" } }
post :create, params
expect(response).to redirect_to(pending_subscriptions_path)
end
it "calls Subscription.create_and_request_confirmation(params)" do
email = "e@example.tld"
start_on = "2015-02-28"
expect(Subscription).to receive(:create_and_request_confirmation)
.with({ email: email, start_on: start_on })
post :create, { subscription: { email: email, start_on: start_on } }
end
it "raises an error if missing params email" do
params = { subscription: { start_on: "2015-09-28" } }
expect {
post :create, params
}.to raise_error ActiveRecord::RecordInvalid
end
end
context "GET confirm" do
it "confirms the subscription" do
subscription = create(:subscription,
email: "e@e.tld",
confirmation_token: Subscription.generate_confirmation_token
)
expect(subscription.confirmed?).to eq(false)
params = { confirmation_token: subscription.confirmation_token }
get :confirm, params
expect(subscription.reload.confirmed?).to eq(true)
expect(assigns(:subscription)).to eq(subscription)
end
it "responds with 404 Not Found for unknown token" do
params = { confirmation_token: "an-unknown-token" }
expect {
get :confirm, params
}.to raise_error ActiveRecord::RecordNotFound
end
end
end
控制器测试的关键点包括:
- 验证正确的模板渲染或重定向
- 检查模型方法是否被正确调用
- 测试参数验证和错误处理
- 确认实例变量被正确赋值
API测试示例:Token认证API
随着前后端分离架构的普及,API测试变得越来越重要。以下是一个API认证端点的测试示例:
require 'rails_helper'
describe '/api/v1/token' do
context 'POST' do
context 'with correct credentials' do
it 'issues opaque and tamper-resistant access token' do
user = create(:user)
parameters = { user: { email: user.email, password: user.password } }.to_json
post '/api/v1/token', parameters, json_request_headers
expect(response).to have_http_status(:created)
expect(response.content_type).to eq('application/json')
expect(json.keys).to eq [:access_token]
expect(json[:access_token]).to be_encrypted_to_hide_token_construction(user.access_tokens.last.unencrypted)
expect(json[:access_token]).to be_signed_to_resist_tampering
end
def be_encrypted_to_hide_token_construction(unencrypted_token)
satisfy('be encrypted to hide token construction') do |actual_token|
!actual_token.include?(unencrypted_token) &&
decrypt_and_verify(actual_token) == unencrypted_token
end
end
def be_signed_to_resist_tampering
satisfy('be signed to resist tampering') do |token|
signature_regex = /--[0-9a-z]{40}\z/
signed = signature_regex =~ token
token_with_wrong_signature = token.next
signed && invalid_signature?(token_with_wrong_signature)
end
end
end
context 'with incorrect credentials' do
it 'responds with 401 Unauthorized for incorrect password' do
user = create(:user)
parameters = {
user: { email: user.email, password: "not #{user.password}" }
}.to_json
respond_without_detailed_exceptions do
post '/api/v1/token', parameters, json_request_headers
end
expect(response).to have_http_status(:unauthorized)
expect(response.content_type).to eq('application/json')
expect(json).to eq(status: '401', error: 'Unauthorized')
expect(AccessToken.count).to eq 0
end
it 'locks user after 10 failed attempts' do
user = create(:user)
respond_without_detailed_exceptions do
10.times do
parameters = { user: { email: user.email, password: SecureRandom.hex } }.to_json
post '/api/v1/token', parameters, json_request_headers
end
expect(user.reload.access_locked?).to eq(true)
correct_params = { user: { email: user.email, password: user.password } }.to_json
post '/api/v1/token', correct_params, json_request_headers
end
expect(user.reload.failed_attempts).to eq(10)
expect(response).to have_http_status(:unauthorized)
end
end
context 'with non-JSON MIME type request' do
it 'responds to wrong Content-Type with 415 Unsupported Media Type' do
user = create(:user)
parameters = { user: { email: user.email, password: user.password } }.to_json
headers = json_request_headers.merge 'Content-Type' => 'text/html; charset=utf-8'
respond_without_detailed_exceptions do
post '/api/v1/token', parameters, headers
end
expect(response).to have_http_status(:unsupported_media_type)
expect(response.content_type).to eq('application/json')
expect(json).to eq(status: '415', error: 'Unsupported Media Type')
end
end
end
end
API测试需要特别关注:
- HTTP状态码和响应头
- JSON响应格式和内容
- 认证和授权逻辑
- 请求格式验证
- 错误处理和安全考虑
功能测试:模拟用户交互流程
功能测试(Feature Test)模拟真实用户与应用的交互,验证关键业务流程的正确性。这类测试通常使用Capybara作为自动化测试工具。
功能测试示例:订阅新闻通讯流程
require 'rails_helper'
feature "Subscribe to newsletter" do
context "in browser with native date input", driver: driver_with(native_date_input: true) do
scenario "subscribes user to newsletter" do
visit_new_subscription
fill_in "Email", with: "buddy@example.tld"
fill_in "Start date", with: "01/01/2015"
click_button "Subscribe"
# be_pending_subscription_page是自定义匹配器
expect(page).to be_pending_subscription_page
expect {
visit_emailed_confirm_subscription_link("buddy@example.tld")
expect(page).to be_confirm_subscription_page(Subscription.last)
.with_subscription_starting_on("January 1st, 2015")
}.to change { Subscription.where(confirmed: true).count }.from(0).to(1)
end
end
context "in browser without native date input", driver: driver_with(native_date_input: false) do
scenario "subscribes user to newsletter" do
visit_new_subscription
fill_in "Email", with: "buddy@example.tld"
fill_in_start_date_with_picker
click_button "Subscribe"
expect(page).to be_pending_subscription_page
expect {
visit_emailed_confirm_subscription_link("buddy@example.tld")
expect(page).to be_confirm_subscription_page(Subscription.last)
.with_subscription_starting_on("January 31st, 2015")
}.to change { Subscription.where(confirmed: true).count }.from(0).to(1)
end
end
private
def fill_in_start_date_with_picker
expect(page).not_to have_css("#subscription_start_on + .picker--opened")
find_field("Start date").click # 打开日期选择器
expect(page).to have_css("#subscription_start_on + .picker--opened")
within "#subscription_start_on + .picker" do
find("select[title='Select a year']").select("2015")
find("select[title='Select a month']").select("January")
find("[aria-label='2015-01-31']", text: "31").click
end
expect(page).not_to have_css("#subscription_start_on + .picker--opened")
expect(page).to have_field("subscription_start_on", with: "2015-01-31")
end
def visit_emailed_confirm_subscription_link(recipient)
open_email recipient, with_subject: "Please confirm"
visit_in_email "Confirm your subscription"
end
def visit_new_subscription
visit "/"
click_link "Subscribe to newsletter"
expect(page).to have_title "Subscribe to our newsletter"
expect(current_path).to eq new_subscription_path
today = Time.zone.today.strftime("%Y-%m-%d")
expect(page).to have_field "Start date", with: today
end
end
功能测试的关键要素:
- 模拟真实用户行为(点击链接、填写表单等)
- 测试不同浏览器/设备的兼容性
- 验证电子邮件等异步交互
- 确认数据库状态在流程结束后正确更新
RSpec测试生态系统:必备工具与配置
一个强大的测试体系离不开完善的工具链。RSpec-Rails-Examples项目展示了如何集成多种测试工具,构建完整的测试生态系统。
测试配置基础:rails_helper.rb
rails_helper.rb是RSpec的核心配置文件,负责加载Rails环境和测试依赖:
ENV["RAILS_ENV"] ||= 'test'
require 'spec_helper'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
# 加载spec/support目录下的所有配置文件
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
# 从文件路径推断测试类型
config.infer_spec_type_from_file_location!
# 包含Factory Girl语法糖
config.include FactoryGirl::Syntax::Methods
# 包含Devise测试助手
config.include Devise::TestHelpers, type: :controller
# 包含自定义匹配器
config.include Matchers
# 包含JSON请求助手
config.include JsonHelper, type: :request
# 配置测试顺序随机化
config.order = :random
Kernel.srand config.seed
end
测试数据管理:Factory Girl
Factory Girl是生成测试数据的强大工具,相比Rails内置的fixtures更加灵活和可维护:
# spec/factories/subscriptions.rb
FactoryGirl.define do
factory :subscription do
email { Faker::Internet.email }
confirmed false
confirmation_token { Subscription.generate_confirmation_token }
start_on { Time.zone.today }
end
factory :confirmed_subscription, parent: :subscription do
confirmed true
confirmed_at { 1.day.ago }
end
end
# spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password { Faker::Internet.password(8) }
confirmed_at { Time.zone.now }
trait :admin do
admin true
end
trait :locked do
access_locked true
failed_attempts 10
end
end
end
使用Factory Girl的优势:
- 支持继承和 trait,减少重复代码
- 可动态生成符合验证规则的数据
- 轻松创建关联对象
- 集成Faker gem生成逼真的测试数据
数据库清理:Database Cleaner
在测试过程中保持数据库清洁是确保测试独立性的关键。Database Cleaner提供了灵活的数据库清理策略:
# spec/support/database_cleaner.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
Database Cleaner策略选择:
- Transaction:最快的策略,使用数据库事务回滚,不支持JavaScript驱动
- Truncation:清空表数据,速度较慢但兼容性好
- Deletion:删除记录但保留表结构,介于两者之间
外部API测试:VCR
VCR可以录制和回放HTTP请求,让依赖外部API的测试变得可靠和快速:
# spec/support/vcr.rb
VCR.configure do |c|
c.cassette_library_dir = "spec/support/http_cache/vcr"
c.hook_into :webmock
c.ignore_localhost = true
c.default_cassette_options = {
record: :new_episodes,
match_requests_on: [:method, :uri, :body]
}
# 过滤敏感信息
c.filter_sensitive_data('<API_KEY>') { ENV['EXTERNAL_API_KEY'] }
end
RSpec.configure do |config|
config.around(:each, :vcr) do |example|
name = example.metadata[:full_description].underscore.gsub(/\s+/, '_')
VCR.use_cassette(name) { example.call }
end
end
# 使用示例
# spec/jobs/headline_scraper_job_spec.rb
it "emails headlines scraped from given URL" do
url_to_scrape = "https://eliotsykes.github.io/rspec-rails-examples/"
recipient_email = "anchor@nightlynews.tld"
assert_performed_with(
job: HeadlineScraperJob,
args: [{url: url_to_scrape, recipient: recipient_email}],
queue: 'default'
) do
VCR.use_cassette("news_page") do
HeadlineScraperJob.perform_later url: url_to_scrape, recipient: recipient_email
end
end
open_email recipient_email, with_subject: "Today's Headlines"
expected_headlines = [
"Man Bites Dog (served by VCR)",
"Dog Presses Charges",
"Cat Dismissed as Unreliable Witness"
]
expected_headlines.each do |expected_headline|
expect(current_email).to have_body_text expected_headline
end
end
VCR的主要优势:
- 加速依赖外部服务的测试
- 允许在没有网络连接的情况下运行测试
- 捕获并隔离API变更
- 保护敏感认证信息
电子邮件测试:Email Spec
Email Spec提供了简洁的语法来测试Rails邮件:
# spec/support/email_spec.rb
RSpec.configure do |config|
config.include EmailSpec::Helpers
config.include EmailSpec::Matchers
end
# 使用示例
# spec/mailers/news_mailer_spec.rb
RSpec.describe NewsMailer, :type => :mailer do
describe ".send_headlines(headlines:, to:)" do
it "has expected subject when there are headlines" do
mail = NewsMailer.send_headlines(
headlines: ["Scaremongering Headline: Be Fearful!"],
to: "newsguy@goodnews.tld"
)
expect(mail).to have_subject("Today's Headlines")
end
[nil, []].each do |no_headlines|
it "has expected subject when there are no headlines '#{no_headlines.inspect}'" do
NewsMailer.send_headlines(headlines: no_headlines, to: "newsguy@goodnews.tld")
expect(open_last_email).to have_subject "No Headlines Today :-("
end
end
it "sends from the default email" do
mail = NewsMailer.send_headlines(headlines: nil, to: "newsguy@goodnews.tld")
expect(mail).to be_delivered_from("e@rspec-rails-examples.tld")
end
context "HTML body" do
it "has the given headlines" do
headlines = ["A headline", "Another headline"]
mail = NewsMailer.send_headlines(headlines: headlines, to: "newsguy@goodnews.tld")
headlines.each do |headline|
html = %Q|<li>#{headline}</li>|
expect(mail).to have_body_text(html)
end
end
end
end
end
Email Spec提供的便捷匹配器:
have_subject:验证邮件主题be_delivered_to/be_delivered_from:验证收件人和发件人have_body_text:检查邮件正文内容have_header:验证邮件头
测试简化:Shoulda Matchers
Shoulda Matchers提供了大量预定义的匹配器,大幅减少测试代码量:
# spec/support/shoulda_matchers.rb
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
# 使用示例
# spec/models/subscription_spec.rb
RSpec.describe Subscription, :type => :model do
context "db" do
context "indexes" do
it { should have_db_index(:email).unique(true) }
it { should have_db_index(:confirmation_token).unique(true) }
end
context "columns" do
it { should have_db_column(:email).of_type(:string).with_options(limit: 100, null: false) }
it { should have_db_column(:confirmed).of_type(:boolean).with_options(default: false, null: false) }
end
end
context "validation" do
let(:subscription) { build(:subscription, confirmation_token: "token", email: "a@b.c") }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should validate_presence_of(:confirmation_token) }
it { should validate_uniqueness_of(:confirmation_token) }
end
end
常用的Shoulda Matchers类别:
- 数据库匹配器:
have_db_column、have_db_index - 验证匹配器:
validate_presence_of、validate_uniqueness_of - 关联匹配器:
belong_to、have_many、have_one - 路由匹配器:
route_to、be_routed_to
高级测试技巧:自定义匹配器与测试模式
自定义匹配器:提升测试可读性
自定义匹配器可以将复杂的断言逻辑封装为语义化的测试语句,大幅提升测试代码的可读性:
# spec/matchers/be_confirm_subscription_page.rb
module Matchers
def be_confirm_subscription_page(subscription)
BeConfirmSubscriptionPage.new(subscription)
end
class BeConfirmSubscriptionPage
def initialize(subscription)
@subscription = subscription
end
def matches?(page)
@page = page
page.has_current_path?(confirm_subscription_path(@subscription)) &&
page.has_content?("Subscription confirmed successfully!") &&
page.has_content?("Thank you for confirming your subscription.")
end
def with_subscription_starting_on(formatted_date)
@formatted_date = formatted_date
self
end
def matches?(page)
original_match = super(page)
return original_match unless @formatted_date
original_match && page.has_content?("Your subscription starts on #{@formatted_date}")
end
def failure_message
if @formatted_date
"expected page to be confirmation page for subscription ##{@subscription.id} with start date #{@formatted_date}"
else
"expected page to be confirmation page for subscription ##{@subscription.id}"
end
end
def failure_message_when_negated
if @formatted_date
"expected page not to be confirmation page for subscription ##{@subscription.id} with start date #{@formatted_date}"
else
"expected page not to be confirmation page for subscription ##{@subscription.id}"
end
end
end
end
# 错误消息匹配器
# spec/matchers/have_error_messages.rb
module Matchers
def have_error_messages(*args)
HaveErrorMessages.new(*args)
end
alias have_error_message have_error_messages
class HaveErrorMessages
attr_accessor :failure_message
def initialize(*args)
@expected_messages = *args
end
def matches?(actual_page)
actual_page.within "#error_explanation" do
expected_error_count = @expected_messages.size
expected_error_count_msg = "#{expected_error_count} #{'error'.pluralize(expected_error_count)} prohibited this user from being saved"
unless actual_page.has_content?(expected_error_count_msg)
self.failure_message = "\nexpected error count message: #{expected_error_count_msg}\n got error count message: #{actual_page.text}\n"
return false
end
actual_page.within "ul" do
unless actual_page.has_css?("li", count: expected_error_count)
self.failure_message = "\nexpected error count: #{expected_error_count}\n"
return false
end
@expected_messages.each do |expected_msg|
unless actual_page.has_selector?("li", text: expected_msg)
self.failure_message = "\nmissing error message: #{expected_msg}\n"
return false
end
end
end
end
true
end
end
end
# 自定义匹配器使用示例
# spec/features/subscribe_to_newsletter_spec.rb
scenario "subscribes user to newsletter" do
visit_new_subscription
fill_in "Email", with: "buddy@example.tld"
fill_in "Start date", with: "01/01/2015"
click_button "Subscribe"
expect(page).to be_pending_subscription_page
expect {
visit_emailed_confirm_subscription_link("buddy@example.tld")
expect(page).to be_confirm_subscription_page(Subscription.last)
.with_subscription_starting_on("January 1st, 2015")
}.to change { Subscription.where(confirmed: true).count }.from(0).to(1)
end
自定义匹配器的优势:
- 提高测试代码的可读性和可维护性
- 封装重复的断言逻辑
- 提供更具表达力的测试失败消息
- 使测试代码更接近自然语言
常用测试模式:应对复杂测试场景
1. 时间敏感代码测试
许多业务逻辑依赖于当前时间,使用时间旅行可以精确控制测试环境的时间:
# spec/models/subscription_spec.rb
describe ".confirmation_overdue" do
before do
# 冻结时间
travel_to Time.zone.now
end
after { travel_back } # 恢复时间
it "returns unconfirmed subscriptions older than 3 days" do
overdue = create(:subscription, confirmed: false, created_at: (3.days + 1.second).ago)
expect(Subscription.confirmation_overdue).to match_array [overdue]
end
it "does not return recent unconfirmed subscriptions" do
create(:subscription, confirmed: false, created_at: 3.days.ago)
expect(Subscription.confirmation_overdue).to be_empty
end
end
Rails提供的时间旅行方法:
travel_to:将时间设置为指定时刻travel:相对时间旅行(向前或向后移动指定时间)freeze_time:冻结时间在当前时刻
2. 异步任务测试
Active Job测试需要验证任务是否入队以及执行结果是否正确:
# spec/support/job_helpers.rb
RSpec.configure do |config|
config.include ActiveJob::TestHelper
config.after(:each) do
clear_enqueued_jobs
clear_performed_jobs
end
end
# spec/jobs/headline_scraper_job_spec.rb
RSpec.describe HeadlineScraperJob, :type => :job do
it "emails headlines scraped from given URL" do
url_to_scrape = "https://eliotsykes.github.io/rspec-rails-examples/"
recipient_email = "anchor@nightlynews.tld"
# 验证作业是否入队
assert_performed_with(
job: HeadlineScraperJob,
args: [{url: url_to_scrape, recipient: recipient_email}],
queue: 'default'
) do
VCR.use_cassette("news_page") do
HeadlineScraperJob.perform_later url: url_to_scrape, recipient: recipient_email
end
end
# 验证作业执行结果
open_email recipient_email, with_subject: "Today's Headlines"
expected_headlines = [
"Man Bites Dog (served by VCR)",
"Dog Presses Charges",
"Cat Dismissed as Unreliable Witness"
]
expected_headlines.each do |expected_headline|
expect(current_email).to have_body_text expected_headline
end
end
end
Active Job测试方法:
assert_enqueued_with:验证作业是否入队assert_performed_with:验证作业是否执行perform_enqueued_jobs:立即执行所有入队作业perform_enqueued_job:执行指定的入队作业
3. 测试环境差异化配置
不同类型的测试可能需要不同的配置,RSpec标签可以实现条件配置:
# spec/support/capybara.rb
RSpec.configure do |config|
# 为JavaScript测试配置不同的驱动
config.before(:each, js: true) do
Capybara.current_driver = :selenium_chrome_headless
end
config.after(:each, js: true) do
Capybara.use_default_driver
end
end
# spec/features/subscribe_to_newsletter_spec.rb
feature "Subscribe to newsletter" do
# 使用原生日期输入的浏览器
context "in browser with native date input", driver: driver_with(native_date_input: true) do
scenario "subscribes user to newsletter" do
# ...测试代码...
end
end
# 不支持原生日期输入的浏览器
context "in browser without native date input", driver: driver_with(native_date_input: false) do
scenario "subscribes user to newsletter" do
# ...测试代码...
end
end
end
常用的测试标签:
:js:标记需要JavaScript支持的测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



