关于 testing-patterns
这是一份面向 Rails 开发者的自动化测试指南,使用 RSpec 和 Capybara,涵盖模型与系统测试实践,并强调可靠的数据准备和元素选择。
主要功能
- 01RSpec 约定与测试结构
- 02使用 FactoryBot 准备数据
- 03用 build 测试验证规则
- 04Capybara 系统测试模式
- 05通过 data-testid 稳定选取元素
适用场景
- 01编写模型验证测试
- 02测试面向用户的 Rails 流程
- 03为列表测试预先创建数据库记录
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rolemodel/ rolemodel-skills --skill testing-patterns安装后在 Claude Code 中输入/testing-patterns确认已加载。
详细介绍
测试模式
概述
使用 RSpec 和 Capybara 编写自动化测试。避免使用 Rails 控制台或启动 Rails 服务器进行测试。
测试命令
bundle exec rspec
技术栈
- RSpec - 测试框架
- Capybara - 系统/集成测试
- FactoryBot - 测试数据生成
最佳实践
通用指南
- 先编写测试,或在实现功能的同时编写测试
- 避免通过控制台手动测试
- 使用工厂创建测试数据
- 保持测试聚焦且易读
RSpec 约定
- 使用描述性强的 context 和 describe 块
- 遵循 arrange-act-assert 模式
- 使用 let 设置测试数据
- 优先使用
let而不是实例变量
let 与 let!(惰性求值与急切求值)
在以下情况使用 let(惰性求值):
- 在测试中显式引用变量
- 希望避免不必要的数据库写入
- 记录创建时会产生需要控制的副作用
在以下情况使用 let!(急切求值):
- 测试运行前,记录必须已存在于数据库中
- 不会直接引用变量,但需要确保记录存在
- 测试搜索记录的查询(例如,索引页面、搜索功能)
- 设置其他记录所依赖的后台数据
示例 - 系统测试:
RSpec.describe 'Job Management', type: :system do
# Use let! - these records must exist in DB for dropdowns and queries
let!(:location) { create(:location, name: 'Downtown Site') }
let!(:superintendent) { create(:user, :superintendent) }
# Use let - only created when explicitly referenced in a test
let(:job) { create(:job, name: 'Test Job', location:, superintendent:) }
it 'shows location in dropdown' do
visit new_job_path
# location must exist in DB for dropdown to display it
expect(page).to have_select('Location', with_options: [location.name])
end
it 'can delete a job' do
visit job_path(job) # job created here when first referenced
click_button 'Delete'
end
end
常见陷阱:
# ❌ WRONG - Test will fail because job2 doesn't exist in DB yet
let(:job1) { create(:job, name: 'Job 1') }
let(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # job1 created when referenced
expect(page).to have_content('Job 2') # FAIL - job2 never referenced, not in DB
end
# ✅ CORRECT - Both jobs exist before test runs
let!(:job1) { create(:job, name: 'Job 1') }
let!(:job2) { create(:job, name: 'Job 2') }
it 'lists all jobs' do
visit jobs_path
expect(page).to have_content('Job 1') # Both jobs already in DB
expect(page).to have_content('Job 2') # Test passes
end
关键要点:
如果你希望看到数据,但不会显式使用对象变量进行交互(例如查看列表或从下拉菜单中选择),请使用 let! 确保记录已存在于数据库中。
验证测试模式
使用包含无效数据的 build 显式测试验证,然后确认模型无效并检查错误消息:
describe 'validations' do
it 'must have a start date' do
membership = build(:membership, start_date: nil)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly "Start date can't be blank"
end
it 'enforces end date must follow start date' do
membership = build(:membership, start_date: 1.year.ago, end_date: 2.years.ago)
expect(membership).not_to be_valid
expect(membership.errors.full_messages).to contain_exactly 'End date must follow start date'
end
it 'permits empty end date' do
membership = build(:membership, start_date: 1.year.ago, end_date: nil)
expect(membership).to be_valid
end
end
要点:
- 使用
build而不是create,以避免写入数据库 - 测试无效和有效两种情况
- 使用
full_messages验证确切的错误消息 - 使用描述性测试名称说明业务规则
- 不要测试关联或枚举
Capybara 系统测试
- 测试面向用户的功能
- 使用带有
dom_id的data-testid属性,可靠地选择元素 - 测试正常路径和边缘情况
- 确保测试具有确定性
- 避免使用 sleep 语句;使用 Capybara 的等待机制,例如等待元素出现的原生断言
- 对运行 javascript 的规格(例如 stimulus 控制器)使用
:js(如it 'does something', :js do)
使用 data-testid 选择元素
使用带有 dom_id 的 data-testid 属性,稳定可靠地选择元素,不易受 UI 变更影响:
视图:
tbody
- @entries.each do |entry|
tr data-testid=dom_id(entry)
td= entry.name
规格:
within(data_test(entry1)) do
click_button 'Submit'
end
优点:
- 不受文本变更影响(描述、标签等)
- 适用于动态内容
- 测试意图清晰明了
- 更易重构视图
避免:
- 基于文本的查找:
within('tr', text: 'Entry 1') - 样式调整时可能变更的 CSS 类选择器
- 过于具体的 DOM 遍历
使用 within 块限定范围
当元素不明确(多个按钮/链接文本相同)时,使用 within 块限定交互范围:
最佳实践:
在以下情况始终使用 within 块:
- 多个元素具有相同的文本/标签
- 与模态框、面板或覆盖层交互
- 处理重复元素(表格行、卡片)
- 测试失败并提示“匹配不明确”错误
Turbo 确认对话框
测试会触发 Turbo 确认对话框的操作时(例如带有 data: { turbo_confirm: 'message' } 的删除按钮),请使用提供的辅助方法。
设置:
创建辅助文件:
# spec/support/turbo_confirm_helper.rb
module TurboConfirmHelper
def accept_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-accept').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
def deny_turbo_confirm
yield
expect(page).to have_css '.confirm-dialog-wrapper--active', wait: 5
sleep(0.5)
within '.confirm-dialog-wrapper--active' do
find('#confirm-cancel').click
end
expect(page).to_not have_css '.confirm-dialog-wrapper--active', wait: 5
end
end
在 RSpec 配置中引入:
# spec/support/helpers.rb
RSpec.configure do |c|
# ...existing code...
c.include TurboConfirmHelper, type: :system
end
在测试中使用:
accept_turbo_confirm do
click_button 'Delete'
end
deny_turbo_confirm do
click_button 'Delete'
end
要点:
- 涉及 Turbo 确认对话框的测试始终使用
:js标签 - 将触发确认对话框的操作作为代码块传递给辅助方法
- 辅助方法会自动等待对话框出现和消失
- 使用
accept_turbo_confirm点击“是,我确定” - 使用
deny_turbo_confirm点击“取消” - 辅助方法包含适当的等待时间和范围限定,以确保可靠性
FactoryBot
- 为所有模型定义工厂
- 使用 trait 表示不同变体
- 保持工厂精简
- 根据需要在测试中覆盖属性
- 始终使用
build或create,而不是直接实例化模型 - 使用
build进行验证测试,以避免写入数据库 - 需要持久化记录时使用
create
后续主题
- Mock 和 stub 模式
- 测试组织策略
- 性能测试
- CI/CD 集成
评论
支持 Markdown