关于 testing
面向 Rails 开发者的测试技能,帮助规划测试类型、编写测试并处理并行执行、CI 和不稳定测试。内容强调测试金字塔、优先使用请求测试以及使用 fixtures。
主要功能
- 01选择合适的 Rails 测试类型
- 02编写请求与系统测试
- 03配置并行测试和 CI
- 04排查数据库与不稳定测试
- 05使用 fixtures 组织测试数据
适用场景
- 01为 Rails 应用建立测试套件
- 02为 HTTP 端点添加请求测试
- 03配置并行测试或 CI 流程
- 04定位系统测试中的波动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill testing安装后在 Claude Code 中输入/testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/ci.mdfixtures.mdhelpers-and-assertions.mdparallel-testing.mdrequest-tests.mdsystem-tests.md详细介绍
Rails 测试专家
为 Rails 应用构建快速、可靠、易维护的测试套件。本技能涵盖 Rails 测试生态系统——测试类型、基础设施,以及应在何处测试哪些内容。
**配套技能:**有关 Minitest 专用断言、fixtures 深入解析、TDD 工作流和 RSpec 到 Minitest 的转换,请参阅 minitest 技能。
何时使用本技能
- 设置或配置 Rails 测试套件
- 在不同测试类型之间进行选择(系统、请求、模型、任务、邮件程序等)
- 使用 Capybara 编写系统测试
- 配置并行测试或 CI 流水线
- 调试不稳定的测试或测试数据库问题
- 为新功能添加测试覆盖
理念
坚守测试金字塔
/\ System Tests (FEW — critical user paths only)
/ \
/____\ Request/Integration Tests (MODERATE)
/ \
/________\ Unit Tests (MANY — models, services, policies, jobs)
**大多数 Agent 的错误都源于颠倒这个金字塔。**为所有内容编写系统测试是头号错误。系统测试比单元测试慢 10–100 倍。一个不稳定的系统测试所浪费的时间,超过 50 个模型测试所浪费的时间。
核心规则
- 测试行为,而非实现——断言结果,而不是方法调用
- 使用 fixtures,而非工厂——快 10–100 倍,且不会级联创建
- 优先使用请求测试,而非系统测试——仅在依赖 JS 的流程中使用系统测试
- 每项测试都相互独立——没有共享状态,也不依赖测试顺序
- 快速反馈 > 全面覆盖——经常运行的快速测试套件,胜过不会运行的慢速套件
选择合适的测试类型
这是最重要的决定。选错会浪费时间编写缓慢、脆弱的测试。
| 测试类型 | 基类 | 速度 | 用途 |
|---|---|---|---|
| 模型 | ActiveSupport::TestCase | ⚡ 快 | 验证、作用域、业务逻辑、回调 |
| 服务/PORO | ActiveSupport::TestCase | ⚡ 快 | 服务对象、表单对象、查询对象 |
| 任务 | ActiveJob::TestCase | ⚡ 快 | 任务行为、入队、重试 |
| 邮件程序 | ActionMailer::TestCase | ⚡ 快 | 邮件内容、收件人、附件 |
| 辅助方法 | ActionView::TestCase | ⚡ 快 | 视图辅助方法的输出 |
| 请求 | ActionDispatch::IntegrationTest | 🔶 中 | HTTP 请求/响应周期、API 端点 |
| 集成 | ActionDispatch::IntegrationTest | 🔶 中 | 无需浏览器的多步工作流 |
| 通道 | ActionCable::Channel::TestCase | ⚡ 快 | 通道订阅、流 |
| 连接 | ActionCable::Connection::TestCase | ⚡ 快 | WebSocket 身份验证、连接标识符 |
| 系统 | ActionDispatch::SystemTestCase | 🐌 慢 | JS 交互、关键端到端用户路径 |
决策树
Does it test business logic with no HTTP needed?
→ Model / Service test
Does it test an HTTP endpoint's response?
→ Request test
Does it require a real browser (JS, complex UI)?
→ System test (keep these minimal)
Does it test email content or delivery?
→ Mailer test
Does it test a background job?
→ Job test
Does it test a view helper method?
→ Helper test
Does it test a WebSocket channel?
→ Channel test
测试目录结构
test/
├── test_helper.rb # Global config
├── application_system_test_case.rb # System test base (created on first system test)
├── controllers/ # Controller/functional tests (legacy — prefer request tests)
├── fixtures/ # YAML fixture data
│ ├── users.yml
│ ├── files/ # File fixtures for uploads
│ └── action_text/rich_texts.yml # Action Text fixtures
├── helpers/ # View helper tests
├── integration/ # Integration tests
├── jobs/ # Job tests
├── mailers/ # Mailer tests
│ └── previews/ # Mailer previews (not tests, but useful)
├── models/ # Model tests (bulk of your tests)
├── channels/ # Action Cable channel tests
│ └── application_cable/ # Connection tests
├── requests/ # Request tests (preferred over controllers/)
├── services/ # Service object tests
├── system/ # System tests (keep small)
└── support/ # Shared test helpers
测试运行器
必备命令
# Run everything (excludes system tests)
bin/rails test
# Run system tests separately
bin/rails test:system
# Run ALL tests including system
bin/rails test:all
# Run a single file
bin/rails test test/models/user_test.rb
# Run a specific test by line number
bin/rails test test/models/user_test.rb:42
# Run a line range
bin/rails test test/models/user_test.rb:10-30
# Run by name pattern
bin/rails test -n /password/
# Run a directory
bin/rails test test/models/
# Fail fast — stop on first failure
bin/rails test --fail-fast
# Verbose output
bin/rails test -v
# Profile slow tests
bin/rails test --profile
# Set seed for reproducibility
bin/rails test --seed 12345
重要标志
| 标志 | 用途 |
|---|---|
-n PATTERN | 按名称筛选测试(字符串或 /regex/) |
-f / --fail-fast | 在首次失败时停止 |
-v / --verbose | 显示每项测试的名称 |
-b / --backtrace | 完整回溯(而不只是应用代码行) |
--profile [N] | 列出最慢的 N 项测试(默认 10) |
-p / --pride | 彩虹输出 🌈 |
-S / --skip CODES | 跳过报告类型(例如,E 表示错误) |
请求测试(HTTP 测试的首选方式)
请求测试会通过中间件栈模拟完整的 HTTP 请求。新代码请使用请求测试,而不是控制器测试。
class ArticlesRequestTest < ActionDispatch::IntegrationTest
setup do
@user = users(:admin)
@article = articles(:published)
end
test "GET /articles returns success" do
get articles_url
assert_response :success
end
test "POST /articles creates with valid params" do
sign_in @user
assert_difference "Article.count", 1 do
post articles_url, params: { article: { title: "New", body: "Content" } }
end
assert_redirected_to article_url(Article.last)
end
test "POST /articles rejects invalid params" do
sign_in @user
assert_no_difference "Article.count" do
post articles_url, params: { article: { title: "" } }
end
assert_response :unprocessable_entity
end
end
请求后可用的内容
response/response.body/response.parsed_bodycookies、flash、session@controller、@request、@response
参阅
references/request-tests.md,了解 JSON API 测试、Turbo Stream 测试、文件上传、身份验证模式和 DOM 断言。
系统测试(谨慎使用)
系统测试通过 Capybara 在真实浏览器中运行。仅用于需要 JavaScript 或复杂 UI 交互的关键路径。
何时编写系统测试
✅ 用户注册/登录流程 ✅ 结账/支付流程 ✅ 复杂的 JS 交互(拖放、模态框、实时搜索) ✅ 依赖 Turbo/Stimulus 的功能 ✅ 带预览的文件上传
何时不编写系统测试
❌ CRUD 操作(使用请求测试)
❌ 验证错误的显示(使用请求测试 + assert_select)
❌ API 端点(使用请求测试)
❌ 无需浏览器即可测试的任何内容
配置
# test/application_system_test_case.rb
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
end
驱动选项:
| 驱动 | 浏览器 | 使用场景 |
|---|---|---|
:selenium + :headless_chrome | Chrome(无头) | 默认选项,适合 CI |
:selenium + :headless_firefox | Firefox(无头) | 替代选项 |
:selenium + :chrome | Chrome(可见) | 调试 |
:cuprite | 通过 CDP 使用 Chrome | 更快,无 Selenium 开销 |
编写系统测试
require "application_system_test_case"
class CheckoutTest < ApplicationSystemTestCase
test "user completes checkout" do
user = users(:buyer)
product = products(:widget)
visit new_session_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_on "Sign in"
visit product_path(product)
click_on "Add to Cart"
click_on "Checkout"
fill_in "Card number", with: "4242424242424242"
click_on "Pay"
assert_text "Order confirmed"
end
end
避免不稳定的系统测试
assert_text、assert_selector会自动等待——请使用它们,而不是page.has_css?- 如果仍不稳定,请添加显式等待:
assert_text "Done", wait: 5 - 切勿使用
sleep——始终使用 Capybara 会等待的断言 - 使用
take_screenshot进行调试;失败的测试会自动截图并保存到tmp/screenshots/
参见
references/system-tests.md,了解 Capybara DSL 参考、Cuprite 设置、JS 交互模式以及 Turbo/Stimulus 测试。
邮件程序测试
class UserMailerTest < ActionMailer::TestCase
test "welcome email" do
email = UserMailer.welcome(users(:new_user))
assert_emails 1 do
email.deliver_now
end
assert_equal ["[email protected]"], email.from
assert_equal [users(:new_user).email], email.to
assert_equal "Welcome!", email.subject
assert_match users(:new_user).name, email.body.to_s
end
end
来自请求测试: assert_emails 1 { post users_url, params: {...} }
已加入队列: assert_enqueued_email_with UserMailer, :welcome, args: [user] { ... }
参见
references/helpers-and-assertions.md,了解多部分邮件、附件、参数化邮件程序和预览。
作业测试
class ProcessOrderJobTest < ActiveJob::TestCase
test "charges the account" do
perform_enqueued_jobs do
ProcessOrderJob.perform_later(orders(:pending))
end
assert orders(:pending).reload.charged?
end
end
验证从其他代码中加入队列的情况:
class OrderTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
test "#place enqueues processing job" do
assert_enqueued_with(job: ProcessOrderJob) do
orders(:new).place!
end
end
end
参见
references/helpers-and-assertions.md,了解作业队列断言、筛选、链式作业和异常测试。
辅助方法测试
class CurrencyHelperTest < ActionView::TestCase
test "formats USD" do
assert_equal "$10.00", format_price(1000, "USD")
end
test "handles zero" do
assert_equal "$0.00", format_price(0, "USD")
end
end
Action Cable 测试
# Channel test
class ChatChannelTest < ActionCable::Channel::TestCase
test "subscribes to room stream" do
subscribe room: "general"
assert subscription.confirmed?
assert_has_stream "chat_general"
end
end
# Connection test
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
test "connects with valid user" do
cookies.signed[:user_id] = users(:admin).id
connect
assert_equal users(:admin).id, connection.current_user.id
end
test "rejects without auth" do
assert_reject_connection { connect }
end
end
广播断言(可用于任何测试): include ActionCable::TestHelper,然后使用 assert_broadcast_on、assert_broadcasts。
参见
references/helpers-and-assertions.md,了解流断言、自定义频道方法以及从模型测试广播。
测试数据库管理
架构同步
# After migrations, sync test DB schema
bin/rails db:test:prepare
# Or recreate from scratch
bin/rails db:test:purge db:test:prepare
# Rails auto-checks for pending migrations before tests run
事务
默认情况下,每个测试都在数据库事务中运行,并在完成后回滚。这样无需手动清理即可隔离测试。
必要时禁用(例如,测试多线程代码时):
class ThreadedTest < ActiveSupport::TestCase
self.use_transactional_tests = false
teardown do
# Manual cleanup needed!
User.delete_all
end
end
并行测试
基于进程(默认)
# test/test_helper.rb
class ActiveSupport::TestCase
parallelize(workers: :number_of_processors)
end
为每个工作进程创建单独的测试数据库(test-database-0、test-database-1 等)。
# Override worker count
PARALLEL_WORKERS=4 bin/rails test
基于线程
class ActiveSupport::TestCase
parallelize(workers: :number_of_processors, with: :threads)
end
适用于 JRuby/TruffleRuby,或进程分叉开销较大的情况。
使用 parallelize_setup/parallelize_teardown 钩子进行每个工作进程的设置。默认阈值为 50 个测试(低于该数量时不会并行执行)。使用 config.active_support.test_parallelization_threshold = 100 自定义阈值。
参见
references/parallel-testing.md,了解进程与线程的对比、钩子以及常见的并行测试问题。
CI 配置
# config/environments/test.rb
config.eager_load = ENV["CI"].present?
# CI steps
bin/rails db:test:prepare
bin/rails test # Unit + request tests
bin/rails test:system # System tests (separate job recommended)
CI 提示: 使用 PARALLEL_WORKERS 并行执行,将系统测试拆分到单独的 CI 作业中,缓存 gems/node_modules,使用无头 Chrome,并添加 Zeitwerk 急切加载测试。
参见
references/ci.md,获取完整的 GitHub Actions YAML 和 CI 模式。
时间相关测试
freeze_time do ... end # Freeze at current moment
travel_to Time.zone.local(2024, 1, 15) do ... end # Travel to specific time
travel 3.days do ... end # Travel forward
travel_back # Return to real time
参见
references/parallel-testing.md,了解详细的时间测试模式、常见陷阱和不稳定测试的修复方法。
应避免的反模式
- 系统测试过多——如果不需要浏览器,就使用请求测试
- 测试 Rails 本身——不要测试
validates :name, presence: true是否有效;要测试你自己的业务逻辑 - 测试实现细节——
assert_received(:method)会让测试依赖内部实现 - 工厂级联——Factory Bot 为一个测试创建 15 条记录;请使用 fixtures
- 共享可变状态——在一个测试中设置的实例变量泄漏到另一个测试中
- 基于休眠的等待——系统测试中使用
sleep 2;请使用 Capybara 内置的等待机制 - 精确时间戳断言——
assert_equal Time.current, record.created_at容易导致测试不稳定 - 测试私有方法——只测试公共接口
- 过度模拟——模拟所有内容意味着你测试的是模拟对象,而不是代码
- 没有负向测试——始终测试哪些情况应该失败或被拒绝
快速参考:该在哪里测试什么
| 内容 | 测试类型 | 示例 |
|---|---|---|
| 模型验证 | 模型测试 | refute User.new(email: nil).valid? |
| 模型作用域 | 模型测试 | assert_includes User.active, @user |
| 业务逻辑方法 | 模型/服务测试 | assert_equal 42, order.total |
| API 端点 | 请求测试 | get api_users_url, as: :json |
| 页面正确渲染 | 请求测试 | get root_url; assert_response :success |
| 表单提交 | 请求测试 | post articles_url, params: {...} |
| 身份验证/授权 | 请求测试 | get admin_url; assert_response :redirect |
| 邮件内容 | 邮件程序测试 | assert_equal "Welcome", email.subject |
| 邮件发送 | 请求测试 | assert_emails 1 { post ... } |
| 作业行为 | 作业测试 | perform_enqueued_jobs { MyJob.perform_later } |
| 作业入队 | 模型/请求测试 | assert_enqueued_with(job: MyJob) { ... } |
| 依赖 JS 的流程 | 系统测试 | click_on "Submit"; assert_text "Done" |
| 视图辅助方法 | 辅助方法测试 | assert_equal "$10", format_price(10) |
| WebSocket 频道 | 频道测试 | subscribe; assert_has_stream "room_1" |