inertia-rails-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inertia Rails Testing

Inertia Rails 测试

Comprehensive guide to testing Inertia Rails applications with RSpec and Minitest.
使用RSpec和Minitest测试Inertia Rails应用的完整指南。

Testing Approaches

测试方法

  1. Endpoint tests - Verify server-side Inertia responses
  2. Client-side unit tests - Test components with Vitest/Jest
  3. End-to-end tests - Full browser testing with Capybara
  1. 端点测试 - 验证服务端Inertia响应
  2. 客户端单元测试 - 使用Vitest/Jest测试组件
  3. 端到端测试 - 使用Capybara进行全浏览器测试

RSpec Setup

RSpec 配置

Add to
spec/rails_helper.rb
:
ruby
require 'inertia_rails/rspec'
添加到
spec/rails_helper.rb
文件中:
ruby
require 'inertia_rails/rspec'

RSpec Matchers Reference

RSpec匹配器参考

be_inertia_response

be_inertia_response

Verifies the response is an Inertia response:
ruby
it 'returns an Inertia response' do
  get users_path
  expect(inertia).to be_inertia_response
end
验证响应是否为Inertia响应:
ruby
it 'returns an Inertia response' do
  get users_path
  expect(inertia).to be_inertia_response
end

render_component

render_component

Checks the rendered component name:
ruby
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
检查渲染的组件名称:
ruby
it 'renders the correct component' do
  get users_path
  expect(inertia).to render_component('users/index')

  # 不区分大小写匹配
  expect(inertia).to render_component('Users/Index')
end

have_props

have_props

Partial matching of props:
ruby
it 'includes expected props' do
  get user_path(user)

  expect(inertia).to have_props(
    user: hash_including(
      id: user.id,
      name: 'John'
    )
  )
end
Props的部分匹配:
ruby
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

结合RSpec匹配器使用

it 'has users array' do get users_path expect(inertia).to have_props( users: be_an(Array), total: be > 0 ) end
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
undefined
it 'includes required keys' do get dashboard_path expect(inertia).to have_props(:user, :stats, :notifications) end
undefined

have_exact_props

have_exact_props

Exact matching of all props:
ruby
it 'has exactly these props' do
  get simple_page_path

  expect(inertia).to have_exact_props(
    title: 'Simple Page',
    content: 'Hello'
  )
end
所有Props的完全匹配:
ruby
it 'has exactly these props' do
  get simple_page_path

  expect(inertia).to have_exact_props(
    title: 'Simple Page',
    content: 'Hello'
  )
end

have_flash

have_flash

Check flash messages:
ruby
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
检查闪存消息:
ruby
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

have_deferred_props

Verify deferred props:
ruby
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
验证延迟Props:
ruby
it 'defers analytics data' do
  get dashboard_path

  expect(inertia).to have_deferred_props(:analytics)
  expect(inertia).to have_deferred_props(analytics: 'stats')  # 指定分组
end

Complete RSpec Examples

完整RSpec示例

Basic Request Specs

基础请求测试

ruby
undefined
ruby
undefined

spec/requests/users_spec.rb

spec/requests/users_spec.rb

require 'rails_helper'
RSpec.describe '/users', type: :request do let(:user) { create(:user) } let(:valid_attributes) { { name: 'John', email: 'john@example.com' } } 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
undefined
require 'rails_helper'
RSpec.describe '/users', type: :request do let(:user) { create(:user) } let(:valid_attributes) { { name: 'John', email: 'john@example.com' } } 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 # 创建用户
  expect {
    delete user_path(user)
  }.to change(User, :count).by(-1)

  expect(response).to redirect_to(users_url)
end
end end
undefined

Testing Shared Data

测试共享数据

ruby
undefined
ruby
undefined

spec/requests/shared_data_spec.rb

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
undefined
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
undefined

Testing Partial Reloads

测试部分重载

ruby
undefined
ruby
undefined

spec/requests/partial_reloads_spec.rb

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
undefined
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)
# 模拟部分重载
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
undefined

Testing Deferred Props

测试延迟Props

ruby
undefined
ruby
undefined

spec/requests/deferred_props_spec.rb

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
undefined
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
undefined

Minitest Setup

Minitest配置

Add to
test/test_helper.rb
:
ruby
require 'inertia_rails/minitest'
添加到
test/test_helper.rb
文件中:
ruby
require 'inertia_rails/minitest'

Minitest Assertions Reference

Minitest断言参考

RSpec MatcherMinitest Assertion
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
Negation:
refute_*
variants available.
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
否定形式:支持
refute_*
系列断言。

Complete Minitest Examples

完整Minitest示例

ruby
undefined
ruby
undefined

test/integration/users_test.rb

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: 'new@example.com' } } 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: 'test@example.com' } } 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
undefined
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: 'new@example.com' } } 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: 'test@example.com' } } 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
undefined

End-to-End Testing with Capybara

使用Capybara进行端到端测试

ruby
undefined
ruby
undefined

spec/system/users_spec.rb

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: 'john@example.com'
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
undefined
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: 'john@example.com'
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
# 记录初始页面加载标记
page.execute_script("window.initialPageLoad = true")

click_link 'New User'

# 仍处于同一页面加载状态(SPA导航)
expect(page.evaluate_script("window.initialPageLoad")).to be true
expect(page).to have_current_path(new_user_path)
end end
undefined

Client-Side Component Testing

客户端组件测试

Vitest/Jest Setup for Vue

Vue项目的Vitest/Jest配置

javascript
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
  },
})
javascript
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
  },
})

Testing Vue Components

Vue组件测试

javascript
// 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')
  })
})
javascript
// 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')
  })
})

Testing React Components

React组件测试

javascript
// 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()
  })
})
javascript
// 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()
  })
})

Testing Best Practices

测试最佳实践

1. Test Response Structure, Not Implementation

1. 测试响应结构,而非实现细节

ruby
undefined
ruby
undefined

Good - tests the contract

推荐 - 测试契约

expect(inertia).to have_props( user: hash_including(:id, :name, :email) )
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)
undefined
expect(inertia.props[:user]).to eq(user.as_json)
undefined

2. Use Factories for Test Data

2. 使用工厂生成测试数据

ruby
undefined
ruby
undefined

spec/factories/users.rb

spec/factories/users.rb

FactoryBot.define do factory :user do name { Faker::Name.name } email { Faker::Internet.email } password { 'password123' } end end
undefined
FactoryBot.define do factory :user do name { Faker::Name.name } email { Faker::Internet.email } password { 'password123' } end end
undefined

3. Test Authorization Results

3. 测试授权结果

ruby
it 'includes permission data' do
  get users_path

  expect(inertia).to have_props(
    can: hash_including(
      create_user: be_in([true, false])
    )
  )
end
ruby
it 'includes permission data' do
  get users_path

  expect(inertia).to have_props(
    can: hash_including(
      create_user: be_in([true, false])
    )
  )
end

4. Test Error States

4. 测试错误状态

ruby
it 'handles not found' do
  get user_path(id: 'nonexistent')

  expect(response).to have_http_status(:not_found)
end
ruby
it 'handles not found' do
  get user_path(id: 'nonexistent')

  expect(response).to have_http_status(:not_found)
end

5. Use Shared Examples for Common Patterns

5. 使用共享示例复用常见模式

ruby
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
ruby
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