mcpField
I
inertia-rails-testing

inertia-rails-testing

github.com/cole-robertson测试与质量3884 次安装31 周前更新
代码仓库

关于 inertia-rails-testing

Inertia Rails Testing 是面向开发者的 Inertia Rails 测试指南,涵盖 RSpec 和 Minitest。内容涉及服务端响应、客户端组件及浏览器端到端测试,并提供针对 Inertia 数据的匹配器与断言。

主要功能

  1. 01用于 Inertia 响应的 RSpec 匹配器
  2. 02用于 Inertia 数据的 Minitest 断言
  3. 03检查 props、flash 和组件
  4. 04测试延迟与部分 props
  5. 05涵盖 Capybara 端到端测试
  6. 06提供 Vue 和 React 组件示例

适用场景

  1. 01验证 Inertia 响应契约
  2. 02测试 props 和 flash 消息
  3. 03检查延迟数据行为
  4. 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 应用的综合指南。

测试方法

  1. 端点测试 - 验证服务端 Inertia 响应
  2. 客户端单元测试 - 使用 Vitest/Jest 测试组件
  3. 端到端测试 - 使用 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_responseassert_inertia_response
render_componentassert_inertia_component
have_propsassert_inertia_props
have_exact_propsassert_inertia_props_equal
have_flashassert_inertia_flash
have_deferred_propsassert_inertia_deferred_props
have_no_propassert_no_inertia_prop
have_view_dataassert_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