inertia-rails-testing
Inertia Rails Testing
Comprehensive guide to testing Inertia Rails applications with RSpec and Minitest.
Testing Approaches
- Endpoint tests - Verify server-side Inertia responses
- Client-side unit tests - Test components with Vitest/Jest
- 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
More from cole-robertson/inertia-rails-skills
inertia-rails-best-practices
Comprehensive best practices for Inertia Rails development. Use when writing, reviewing, or refactoring Inertia.js Rails applications with React, Vue, or Svelte frontends. Covers server-side setup, props management, forms, navigation, performance, security, and testing patterns.
68inertia-rails-performance
Optimize Inertia Rails application performance. Use when implementing code splitting, prefetching, deferred props, infinite scrolling, polling, or other performance optimizations.
54inertia-rails-auth
Implement authentication and authorization in Inertia Rails applications. Use when setting up login, sessions, permissions, and access control with Devise, has_secure_password, or other auth solutions.
53inertia-rails-forms
Build forms in Inertia Rails applications with proper validation, file uploads, and error handling. Use when implementing forms, handling validation errors, or working with file uploads in Inertia.js with Rails.
52inertia-rails-cookbook
Recipes and patterns for common Inertia Rails use cases. Includes modal dialogs, shadcn/ui integration, search with filters, wizard flows, and other advanced patterns.
52inertia-rails-ssr
Set up Server-Side Rendering (SSR) for Inertia Rails applications. Use when you need SEO optimization, faster initial page loads, or support for users with JavaScript disabled.
49