mcpField
T
testing-patterns

testing-patterns

github.com/rolemodel测试与质量737 次安装1 天前更新
代码仓库

关于 testing-patterns

这是一份面向 Rails 开发者的自动化测试指南,使用 RSpec 和 Capybara,涵盖模型与系统测试实践,并强调可靠的数据准备和元素选择。

主要功能

  1. 01RSpec 约定与测试结构
  2. 02使用 FactoryBot 准备数据
  3. 03用 build 测试验证规则
  4. 04Capybara 系统测试模式
  5. 05通过 data-testid 稳定选取元素

适用场景

  1. 01编写模型验证测试
  2. 02测试面向用户的 Rails 流程
  3. 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 块:

  1. 多个元素具有相同的文本/标签
  2. 与模态框、面板或覆盖层交互
  3. 处理重复元素(表格行、卡片)
  4. 测试失败并提示“匹配不明确”错误
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