大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
测试与调试
这一课你将能做到
- 用 Rails 默认的 Minitest 写模型测试和请求测试,跑通
bin/rails test - 用 Fixtures 准备测试数据,说清楚 fixtures 与工厂模式的取舍
- 用 rails console、debug gem 和日志快速定位问题,不盲猜
为什么要学
测试是工程质量的刹车片,不是交付后的锦上添花。Rails 把测试框架、数据准备、运行命令全内置了,创建模型或 scaffold 时测试文件已自动生成——你的成本只剩写断言。和本站对比:Spring Boot 需要手动引入 JUnit + Mockito + 测试配置文件;Rails 开箱即用,「写测试的启动成本趋近于零」,这是社区文化的一部分。
先建立图像
Rails 测试层次:
模型测试(最常用) → 测试业务规则、验证、关联
│
▼
请求测试(Controller) → 模拟 HTTP,断言响应码/重定向/DB 变化
│
▼
系统测试(Capybara) → 模拟真实浏览器点击填表单
│
▼
功能/辅助测试 → helper 方法、任务执行
测试数据(Fixtures)
│
▼
test/fixtures/users.yml
test/fixtures/articles.yml
│
▼
每条测试前自动加载到 test 数据库(每测一事务回滚)
跟着做
步骤 1:模型测试
# test/models/article_test.rb
require "test_helper"
class ArticleTest < ActiveSupport::TestCase
test "标题必填" do
article = Article.new(title: "", body: "正文")
assert_not article.valid?
assert_includes article.errors[:title], "can't be blank"
end
test "发布时必须有正文" do
article = articles(:published_no_body) # 引用 fixture
assert_not article.valid?
assert_includes article.errors[:body], "发布前必须填写正文"
end
test "标题唯一" do
existing = articles(:one)
duplicate = Article.new(title: existing.title, body: "正文")
assert_not duplicate.save
assert_includes duplicate.errors[:title], "has already been taken"
end
end
运行:
bin/rails test test/models/article_test.rb
步骤 2:Fixture 数据准备
test/fixtures/articles.yml:
one:
title: "Rails 概览"
body: "这是第一篇测试文章"
published: true
user: alice # 关联 fixture
two:
title: "路由设计"
body: "路由很重要"
published: false
user: bob
published_no_body:
title: "空正文"
body: ""
published: true
test/fixtures/users.yml:
alice:
name: Alice
email: alice@example.com
password_digest: <%= BCrypt::Password.create("secret123") %>
bob:
name: Bob
email: bob@example.com
password_digest: <%= BCrypt::Password.create("secret123") %>
Fixture 支持 ERB(密码需动态哈希)。每条测试都在事务中运行,测试结束后自动回滚,数据互不污染。
步骤 3:请求测试
# test/controllers/articles_controller_test.rb
require "test_helper"
class ArticlesControllerTest < ActionDispatch::IntegrationTest
test "未登录不能新建文章" do
get new_article_url
assert_redirected_to login_url
end
test "创建文章并重定向" do
login_as users(:alice) # 自定义测试辅助方法
assert_difference "Article.count", 1 do
post articles_url, params: { article: { title: "新文章", body: "正文正文正文" } }
end
assert_redirected_to article_url(Article.last)
follow_redirect!
assert_select "h1", "新文章"
end
end
# test/test_helper.rb 里加辅助方法
class ActionDispatch::IntegrationTest
def login_as(user)
post login_url, params: { email: user.email, password: "secret123" }
end
end
assert_difference 检查数据库计数变化;follow_redirect! 跟随重定向继续断言;assert_select 用 CSS 选择器断言 HTML 内容。
步骤 4:系统测试(浏览器级)
# test/system/articles_test.rb
require "application_system_test_case"
class ArticlesTest < ApplicationSystemTestCase
test "新建文章完整流程" do
login_as users(:alice)
visit articles_url
click_on "写新文章"
fill_in "标题", with: "系统测试文章"
fill_in "正文", with: "这是系统测试的正文内容,要够长。"
click_on "保存"
assert_text "文章创建成功"
assert_text "系统测试文章"
end
end
bin/rails test:system # 跑系统测试(启动 Chrome headless)
系统测试最慢但覆盖最完整,CI 上定期跑,日常开发以模型/请求测试为主。
步骤 5:调试工具箱
日志:log/test.log、log/development.log 看每条 SQL、参数、耗时。开发模式控制台直接输 SQL。
rails console:
# sandbox 模式:自动回滚,放心造数据
bin/rails console --sandbox
# 调试完 C-x C-c 退出,数据全回滚
debug gem:代码里 debugger 或 binding.break,执行时停住:
def create
@article = Article.new(article_params)
debugger # ← 断点
@article.save
end
bin/rails server
# 请求走到断点后,终端进入调试器
# s: step in, n: next, c: continue, p 变量名: 查看
开发模式错误页面:Rails 开发环境报错时自带 Web Console 和本地变量查看,是排查问题的起点。
讲清楚要点
- 测试数据库完全独立:test 环境用
config/database.yml里独立的 test 数据库,绝不会污染开发/生产数据。 - 事务回滚保证隔离:每测一层封装事务,测试结束自动 rollback。只有在系统测试(跨进程真实浏览器)里才用 truncation 策略。
- Fixtures vs Factory:Fixtures 是预定义静态数据,跑得快但维护成本高;Factory(FactoryBot gem)按需构造,测试意图更明确。Rails 默认 fixtures,项目大了可迁移。
- 测什么?什么不测? 测业务规则(验证、回调、权限)、测请求流(响应码、重定向、副作用)、测系统流(关键路径)。不测框架本身、不测 getter/setter、不测纯 CSS。
- CI 必须跑测试:
bin/rails test退出码非零即失败。GitHub Actions 或自建 CI 一行命令集成,阻止有问题的代码合入主干。
你可能会懵的地方
-
fixture 里的关联怎么写? YAML 里用
user: alice引用其他 fixture 的 key。关联名要对应模型的belongs_to名(如user对应belongs_to :user)。 -
assert_select怎么选中文字?assert_select "h1", "新文章"表示「存在 h1 元素且其文本恰好是‘新文章’」。高级用法可查 Capybara/Minitest 文档。 -
测试跑很慢? 先看瓶颈在哪:
bin/rails test --verbose看单测耗时;系统测试最慢,可按 suite 拆分并行跑;滥用sleep是大忌。 -
Minitest 和 RSpec 怎么选? Rails 默认 Minitest,零配置、和框架深度集成。RSpec DSL 更丰富、生态插件多,社区偏好各占一半。初学者先用默认,团队已有 RSpec 项目再跟。
-
spring 缓存导致测试代码改了不生效?
bin/spring stop重启预加载器,或DISABLE_SPRING=1 bin/rails test临时禁用。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 不写测试直接上线 | 改坏功能不自知 | scaffold 已生成测试文件,填断言即可 |
| 测试依赖特定执行顺序 | fixture 数据跨测试泄露(不应发生) | 每条测试数据独立,用 transaction 隔离 |
assert true 凑覆盖率 | 伪测试骗过自己 | 断言具体业务结果,如 assert_redirected_to |
| 系统测试里连数据库断言 | 不同进程,事务隔离失效 | 系统测试断言 UI 可见结果,不用 assert_difference |
调试全靠 puts | 信息量少、改动代码麻烦 | debugger 断点 + 交互式查看变量 |
卡点急救
bin/rails test报ActiveRecord::PendingMigrationError:测试前要求所有迁移执行完,跑bin/rails db:migrate RAILS_ENV=test。- 系统测试报 chromedriver 找不到:安装 Chrome for Testing 和对应 chromedriver,或改配置用 selenium-webdriver 自动下载。
- 测试通过但开发环境报错:环境差异(数据库版本、环境变量)——检查
.env配置、检查 test 是否用 mock 绕过了真实逻辑。
面试常问
-
Rails 测试金字塔怎么分层? 答:单元测试(模型/辅助方法)占主体,快且稳定;请求测试覆盖控制器流转和 HTTP 契约;系统测试走真实浏览器覆盖关键业务流。比例约 70:20:10。测单元抓业务规则,测请求抓接口契约,测系统抓用户价值。
-
Fixtures 和 Factory 的优劣? 答:Fixtures 预定义、加载快、共享度高,但隐式依赖多(新同事不知道每条 fixture 关联了什么);Factory 按需创建、意图明确、可读性强,但构造慢、在重型测下可能拖慢套件。中小型项目 fixtures 足够,大型或微服务项目倾向工厂模式。
-
怎么测需要登录的接口? 答:请求测试里写 helper 方法
login_as(user)(模拟 POST 到 login 路径存 session);或用sign_in(若用 Devise)。关键是测试层面建立会话态,而不是测试逻辑依赖外部认证服务。
自测三问
- 为什么每条测试跑完后数据不会残留?
- 模型测试和请求测试各覆盖什么?不写请求测试只写模型测试有什么风险?
debugger停在代码里后,哪些调试命令最常用?
小结一句话
测试是刹车片不是装饰品:Minitest 开箱即用,fixtures 备数据,请求测试验流程,断点替代盲猜。
延伸
- Testing Rails Applications(官方测试指南)
- 下一课:部署与运维——Puma、Docker、Kamal 与 Solid Cache。