关于 inertia-rails-testing
Inertia Rails Testing 是面向开发者的 Inertia Rails 测试指南,涵盖 RSpec 和 Minitest。内容涉及服务端响应、客户端组件及浏览器端到端测试,并提供针对 Inertia 数据的匹配器与断言。
主要功能
- 01用于 Inertia 响应的 RSpec 匹配器
- 02用于 Inertia 数据的 Minitest 断言
- 03检查 props、flash 和组件
- 04测试延迟与部分 props
- 05涵盖 Capybara 端到端测试
- 06提供 Vue 和 React 组件示例
适用场景
- 01验证 Inertia 响应契约
- 02测试 props 和 flash 消息
- 03检查延迟数据行为
- 04在浏览器中测试 Rails 流程
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ cole-robertson/ inertia-rails-skills --skill inertia-rails-testing安装后在 Claude Code 中输入/inertia-rails-testing确认已加载。
详细介绍
Inertia Rails 测试
使用 RSpec 和 Minitest 测试 Inertia Rails 应用的综合指南。
测试方法
- 端点测试 - 验证服务端 Inertia 响应
- 客户端单元测试 - 使用 Vitest/Jest 测试组件
- 端到端测试 - 使用 Capybara 进行完整的浏览器测试
RSpec 设置
添加到 spec/rails_helper.rb:
require 'inertia_rails/rspec'
RSpec 匹配器参考
be_inertia_response
验证响应是否为 Inertia 响应:
it 'returns an Inertia response' do
get users_path
expect(inertia).to be_inertia_response
end
render_component
检查渲染的组件名称:
it 'renders the correct component' do
get users_path
expect(inertia).to render_component('users/index')
# Case-insensitive matching
expect(inertia).to render_component('Users/Index')
end
have_props
对 props 进行部分匹配:
it 'includes expected props' do
get user_path(user)
expect(inertia).to have_props(
user: hash_including(
id: user.id,
name: 'John'
)
)
end
# With RSpec matchers
it 'has users array' do
get users_path
expect(inertia).to have_props(
users: be_an(Array),
total: be > 0
)
end
# Check for specific keys
it 'includes required keys' do
get dashboard_path
expect(inertia).to have_props(:user, :stats, :notifications)
end
have_exact_props
对所有 props 进行精确匹配:
it 'has exactly these props' do
get simple_page_path
expect(inertia).to have_exact_props(
title: 'Simple Page',
content: 'Hello'
)
end
have_flash
检查 flash 消息:
it 'shows success message' do
post users_path, params: { user: valid_attributes }
follow_redirect!
expect(inertia).to have_flash(notice: 'User created!')
end
it 'shows error message' do
delete user_path(admin_user)
follow_redirect!
expect(inertia).to have_flash(alert: 'Cannot delete admin')
end
have_deferred_props
验证延迟 props:
it 'defers analytics data' do
get dashboard_path
expect(inertia).to have_deferred_props(:analytics)
expect(inertia).to have_deferred_props(analytics: 'stats') # with group
end
have_no_prop
验证某个 prop 不存在:
it 'does not expose sensitive data' do
get user_path(user)
expect(inertia).to have_no_prop(:password_digest)
expect(inertia).to have_no_prop(:remember_token)
end
have_view_data / have_exact_view_data
检查视图数据(仅在服务端使用、不发送给客户端的数据):
it 'includes view data' do
get users_path
expect(inertia).to have_view_data(total: 5)
expect(inertia).to have_exact_view_data(total: 5)
end
完整的 RSpec 示例
基本请求规格
# spec/requests/users_spec.rb
require 'rails_helper'
RSpec.describe '/users', type: :request do
let(:user) { create(:user) }
let(:valid_attributes) { { name: 'John', email: '[email protected]' } }
let(:invalid_attributes) { { name: '', email: 'invalid' } }
describe 'GET /users' do
before { create_list(:user, 3) }
it 'renders the index component with users' do
get users_path
expect(inertia).to be_inertia_response
expect(inertia).to render_component('users/index')
expect(inertia).to have_props(
users: have_attributes(size: 3)
)
end
end
describe 'GET /users/:id' do
it 'renders the show component' do
get user_path(user)
expect(inertia).to render_component('users/show')
expect(inertia).to have_props(
user: hash_including(
id: user.id,
name: user.name,
email: user.email
)
)
end
it 'does not expose sensitive data' do
get user_path(user)
user_props = inertia.props[:user]
expect(user_props).not_to have_key(:password_digest)
expect(user_props).not_to have_key(:remember_token)
end
end
describe 'GET /users/new' do
it 'renders the new component' do
get new_user_path
expect(inertia).to render_component('users/new')
end
end
describe 'POST /users' do
context 'with valid parameters' do
it 'creates a new user and redirects' do
expect {
post users_path, params: { user: valid_attributes }
}.to change(User, :count).by(1)
expect(response).to redirect_to(users_url)
end
it 'shows success flash' do
post users_path, params: { user: valid_attributes }
follow_redirect!
expect(inertia).to have_flash(notice: /created/i)
end
end
context 'with invalid parameters' do
it 'does not create a user' do
expect {
post users_path, params: { user: invalid_attributes }
}.not_to change(User, :count)
end
it 'returns validation errors' do
post users_path, params: { user: invalid_attributes }
follow_redirect!
expect(inertia).to have_props(
errors: hash_including(:name, :email)
)
end
end
end
describe 'PATCH /users/:id' do
context 'with valid parameters' do
it 'updates the user' do
patch user_path(user), params: { user: { name: 'Updated' } }
expect(user.reload.name).to eq('Updated')
expect(response).to redirect_to(user_url(user))
end
end
context 'with invalid parameters' do
it 'returns validation errors' do
patch user_path(user), params: { user: { email: 'invalid' } }
follow_redirect!
expect(inertia).to have_props(errors: hash_including(:email))
end
end
end
describe 'DELETE /users/:id' do
it 'destroys the user' do
user # create the user
expect {
delete user_path(user)
}.to change(User, :count).by(-1)
expect(response).to redirect_to(users_url)
end
end
end
测试共享数据
# spec/requests/shared_data_spec.rb
RSpec.describe 'Shared data', type: :request do
describe 'authentication data' do
context 'when logged in' do
before { sign_in(user) }
it 'includes current user' do
get root_path
expect(inertia).to have_props(
auth: hash_including(
user: hash_including(id: user.id)
)
)
end
end
context 'when logged out' do
it 'has null user' do
get root_path
expect(inertia).to have_props(
auth: hash_including(user: nil)
)
end
end
end
end
测试部分重新加载
# spec/requests/partial_reloads_spec.rb
RSpec.describe 'Partial reloads', type: :request do
it 'supports partial reload of specific props' do
get dashboard_path
expect(inertia).to have_props(:users, :stats, :notifications)
# Simulate partial reload
inertia_reload_only(:users)
expect(inertia).to have_props(:users)
expect(inertia.props.keys).to eq([:users])
end
it 'supports excluding props' do
get dashboard_path
inertia_reload_except(:notifications)
expect(inertia).to have_props(:users, :stats)
expect(inertia).not_to have_props(:notifications)
end
end
测试延迟 props
# spec/requests/deferred_props_spec.rb
RSpec.describe 'Deferred props', type: :request do
it 'initially defers expensive data' do
get dashboard_path
expect(inertia).to have_deferred_props(:analytics)
expect(inertia.props).not_to have_key(:analytics)
end
it 'loads deferred props on request' do
get dashboard_path
inertia_load_deferred_props
expect(inertia).to have_props(
analytics: be_present
)
end
end
Minitest 设置
添加到 test/test_helper.rb:
require 'inertia_rails/minitest'
Minitest 断言参考
| RSpec 匹配器 | Minitest 断言 |
|---|---|
be_inertia_response | assert_inertia_response |
render_component | assert_inertia_component |
have_props | assert_inertia_props |
have_exact_props | assert_inertia_props_equal |
have_flash | assert_inertia_flash |
have_deferred_props | assert_inertia_deferred_props |
have_no_prop | assert_no_inertia_prop |
have_view_data | assert_inertia_view_data |
否定断言:提供 refute_* 变体。
完整的 Minitest 示例
# test/integration/users_test.rb
require 'test_helper'
class UsersTest < ActionDispatch::IntegrationTest
def setup
@user = users(:one)
end
test 'index renders users list' do
get users_path
assert_inertia_response
assert_inertia_component 'users/index'
assert_inertia_props users: ->(users) { users.is_a?(Array) }
end
test 'show renders user details' do
get user_path(@user)
assert_inertia_component 'users/show'
assert_inertia_props(
user: {
id: @user.id,
name: @user.name
}
)
end
test 'create with valid params redirects' do
assert_difference 'User.count', 1 do
post users_path, params: {
user: { name: 'New User', email: '[email protected]' }
}
end
assert_redirected_to users_url
end
test 'create with invalid params shows errors' do
post users_path, params: { user: { name: '' } }
follow_redirect!
assert_inertia_props errors: { name: ["can't be blank"] }
end
test 'shows flash after successful create' do
post users_path, params: {
user: { name: 'Test', email: '[email protected]' }
}
follow_redirect!
assert_inertia_flash notice: 'User created!'
end
test 'deferred props are loaded separately' do
get dashboard_path
assert_inertia_deferred_props :analytics
inertia_load_deferred_props
assert_inertia_props analytics: ->(data) { data.present? }
end
end
测试配置(v3)
求值可选和延迟 props
默认情况下,测试中不会对可选和延迟 props 求值。启用求值以进行全面测试:
# spec/rails_helper.rb (RSpec)
InertiaRails::Testing.evaluate_optional_props = true
# test/test_helper.rb (Minitest)
InertiaRails::Testing.evaluate_optional_props = true
启用此选项后,你可以测试延迟和可选 props 的实际值:
it 'returns analytics data when deferred props are loaded' do
InertiaRails::Testing.evaluate_optional_props = true
get dashboard_path
inertia_load_deferred_props
expect(inertia).to have_props(
analytics: hash_including(:visitors, :page_views)
)
end
使用 Capybara 进行端到端测试
# spec/system/users_spec.rb
require 'rails_helper'
RSpec.describe 'Users', type: :system do
before do
driven_by(:selenium_chrome_headless)
end
it 'creates a new user' do
visit new_user_path
fill_in 'Name', with: 'John Doe'
fill_in 'Email', with: '[email protected]'
fill_in 'Password', with: 'password123'
click_button 'Create User'
expect(page).to have_content('User created successfully')
expect(page).to have_content('John Doe')
end
it 'shows validation errors' do
visit new_user_path
fill_in 'Name', with: ''
click_button 'Create User'
expect(page).to have_content("Name can't be blank")
end
it 'navigates without full page reload' do
visit users_path
# Capture initial page load marker
page.execute_script("window.initialPageLoad = true")
click_link 'New User'
# Still on same page load (SPA navigation)
expect(page.evaluate_script("window.initialPageLoad")).to be true
expect(page).to have_current_path(new_user_path)
end
end
客户端组件测试
Vue 的 Vitest/Jest 设置
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
},
})
测试 Vue 组件
// tests/pages/users/index.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import UsersIndex from '@/pages/users/index.vue'
describe('UsersIndex', () => {
it('renders users list', () => {
const wrapper = mount(UsersIndex, {
props: {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
},
})
expect(wrapper.text()).toContain('John')
expect(wrapper.text()).toContain('Jane')
})
it('shows empty state when no users', () => {
const wrapper = mount(UsersIndex, {
props: { users: [] },
})
expect(wrapper.text()).toContain('No users found')
})
})
测试 React 组件
// tests/pages/users/index.test.jsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import UsersIndex from '@/pages/users/index'
describe('UsersIndex', () => {
it('renders users list', () => {
render(
<UsersIndex
users={[
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]}
/>
)
expect(screen.getByText('John')).toBeInTheDocument()
expect(screen.getByText('Jane')).toBeInTheDocument()
})
})
测试最佳实践
1. 测试响应结构,而非实现
# Good - tests the contract
expect(inertia).to have_props(
user: hash_including(:id, :name, :email)
)
# Avoid - too coupled to implementation
expect(inertia.props[:user]).to eq(user.as_json)
2. 使用工厂生成测试数据
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.name }
email { Faker::Internet.email }
password { 'password123' }
end
end
3. 测试授权结果
it 'includes permission data' do
get users_path
expect(inertia).to have_props(
can: hash_including(
create_user: be_in([true, false])
)
)
end
4. 测试错误状态
it 'handles not found' do
get user_path(id: 'nonexistent')
expect(response).to have_http_status(:not_found)
end
5. 对常见模式使用共享示例
RSpec.shared_examples 'requires authentication' do
it 'redirects to login' do
expect(response).to redirect_to(login_url)
end
end
describe 'GET /admin/users' do
context 'when not logged in' do
before { get admin_users_path }
it_behaves_like 'requires authentication'
end
end
评论
支持 Markdown