Agent Skills: Inertia Rails Testing

Test Inertia Rails applications with RSpec or Minitest. Use when writing tests for Inertia responses, components, props, flash messages, and partial reloads.

UncategorizedID: cole-robertson/inertia-rails-skills/inertia-rails-testing

Install this agent skill to your local

pnpm dlx add-skill https://github.com/cole-robertson/inertia-rails-skills/tree/HEAD/skills/inertia-rails-testing

Skill Files

Browse the full folder contents for inertia-rails-testing.

Download Skill

Loading file tree…

skills/inertia-rails-testing/SKILL.md

Skill Metadata

Name
inertia-rails-testing
Description
Test Inertia Rails applications with RSpec or Minitest. Use when writing tests for Inertia responses, components, props, flash messages, and partial reloads.

Inertia Rails Testing

Comprehensive guide to testing Inertia Rails applications with RSpec and Minitest.

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

RSpec Setup

Add to spec/rails_helper.rb:

require 'inertia_rails/rspec'

RSpec Matchers Reference

be_inertia_response

Verifies the response is an Inertia response:

it 'returns an Inertia response' do
  get users_path
  expect(inertia).to be_inertia_response
end

render_component

Checks the rendered component name:

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

Partial matching of 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

Exact matching of all 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

Check flash messages:

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

Verify deferred 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

Verify a prop is not present:

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

Check view data (server-side only data not sent to client):

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

Complete RSpec Examples

Basic Request Specs

# 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

Testing Shared Data

# 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

Testing Partial Reloads

# 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

Testing Deferred 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 Setup

Add to test/test_helper.rb:

require 'inertia_rails/minitest'

Minitest Assertions Reference

| RSpec Matcher | Minitest 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 | | have_no_prop | assert_no_inertia_prop | | have_view_data | assert_inertia_view_data |

Negation: refute_* variants available.

Complete Minitest Examples

# 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

Testing Configuration (v3)

Evaluate Optional and Deferred Props

By default, optional and deferred props are not evaluated in tests. Enable evaluation for thorough testing:

# spec/rails_helper.rb (RSpec)
InertiaRails::Testing.evaluate_optional_props = true

# test/test_helper.rb (Minitest)
InertiaRails::Testing.evaluate_optional_props = true

With this enabled, you can test the actual values of deferred and optional 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

End-to-End Testing with 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: '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

Client-Side Component Testing

Vitest/Jest Setup for Vue

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

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

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

# 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. Use Factories for Test Data

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.email }
    password { 'password123' }
  end
end

3. Test Authorization Results

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

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

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