Testing
There are many different ways to test an Inertia application. This page provides a quick overview of the tools available.
End-to-end Tests
One popular approach to testing your JavaScript page components is to use an end-to-end testing tool like Cypress or Pest. These are browser automation tools that allow you to run real simulations of your app in the browser. These tests are known to be slower; however, since they test your application at the same layer as your end users, they can provide a lot of confidence that your app is working correctly. And, since these tests are run in the browser, your JavaScript code is actually executed and tested as well.
Client-Side Unit Tests
Another approach to testing your page components is using a client-side unit testing framework, such as Vitest, Jest, or Mocha. This approach allows you to test your JavaScript page components in isolation using Node.js.
Endpoint Tests
In addition to testing your JavaScript page components, you will likely want to also test the Inertia responses that are returned by your server-side framework. A popular approach to doing this is using endpoint tests, where you make requests to your application and examine the responses. Laravel provides tooling for executing these types of tests.
However, to make this process even easier, Inertia’s Laravel adapter provides additional HTTP testing tools. Let’s take a look at an example.
using InertiaCore.Testing;using Microsoft.AspNetCore.Mvc.Testing;
public class PodcastsControllerTest : IClassFixture<WebApplicationFactory<Program>>{ private readonly HttpClient _client;
public PodcastsControllerTest( WebApplicationFactory<Program> factory) { _client = factory.CreateClient(); }
[Fact] public async Task CanViewPodcast() { var request = InertiaRequest.Get("/podcasts/41").Build(); var response = await _client.SendAsync(request);
response.AssertInertia(page => page .Component("Podcasts/Show") .Has("podcast") .Where("podcast.id", podcast.Id) .Where("podcast.subject", "The Laravel Podcast") .Where("podcast.description", "The Laravel Podcast brings you Laravel and PHP development news and discussion.") .Count("podcast.seasons", 4) .Count("podcast.seasons.4.episodes", 21) .Where("podcast.host.id", 1) .Where("podcast.host.name", "Matt Stauffer") .Count("podcast.subscribers", 7) .Where("podcast.subscribers.0.id", 2) .Where("podcast.subscribers.0.name", "Claudio Dekker") .Where("podcast.subscribers.0.platform", "Apple Podcasts") .Missing("podcast.subscribers.0.email") .Missing("podcast.subscribers.0.password")); }}use Inertia\Testing\AssertableInertia as Assert;
class PodcastsControllerTest extends TestCase{ public function test_can_view_podcast() { $this->get('/podcasts/41') ->assertInertia(fn (Assert $page) => $page ->component('Podcasts/Show') ->has('podcast', fn (Assert $page) => $page ->where('id', $podcast->id) ->where('subject', 'The Laravel Podcast') ->where('description', 'The Laravel Podcast brings you Laravel and PHP development news and discussion.') ->has('seasons', 4) ->has('seasons.4.episodes', 21) ->has('host', fn (Assert $page) => $page ->where('id', 1) ->where('name', 'Matt Stauffer') ) ->has('subscribers', 7, fn (Assert $page) => $page ->where('id', 2) ->where('name', 'Claudio Dekker') ->where('platform', 'Apple Podcasts') ->etc() ->missing('email') ->missing('password') ) ) ); }}As you can see in the example above, you may use these assertion methods to assert against the content of the data provided to the Inertia response. In addition, you may assert that array data has a given length as well as scope your assertions.
You may use the inertiaProps method to retrieve the props returned in the response. You can pass a key to retrieve a specific property, and nested properties are supported using “dot” notation.
// InertiaCore exposes props through the fluent AssertablePage// API rather than a dedicated prop getter. Use Has/Where with// dot-notation paths to walk into the response.var request = InertiaRequest.Get("/podcasts/41").Build();var response = await _client.SendAsync(request);
response.AssertInertia(page => page // Check that a specific prop exists... .Has("podcast")
// Assert against a nested prop using "dot" notation... .Has("podcast.id"));$response = $this->get('/podcasts/41');
// Returns all props...$response->inertiaProps();
// Returns a specific prop...$response->inertiaProps('podcast');
// Returns a nested prop using "dot" notation...$response->inertiaProps('podcast.id');Let’s dig into the assertInertia method and the available assertions in detail. First, to assert that the Inertia response has a property, you may use the has method. You can think of this method as being similar to PHP’s isset function.
response.AssertInertia(page => page // Checking a root-level prop... .Has("podcast")
// Checking nested props using "dot" notation... .Has("podcast.id"));$response->assertInertia(fn (Assert $page) => $page // Checking if a root-level property has 7 items... ->has('podcasts', 7)
// Checking nested properties using "dot" notation... ->has('podcast.subscribers', 7));To assert that an Inertia property has a specified amount of items, you may provide the expected size as the second argument to the has method.
response.AssertInertia(page => page // Checking if a root-level prop has 7 items... .Count("podcasts", 7)
// Checking nested props using "dot" notation... .Count("podcast.subscribers", 7));$response->assertInertia(fn (Assert $page) => $page // Checking if a root-level property has 7 items... ->has('podcasts', 7)
// Checking nested properties using "dot" notation... ->has('podcast.subscribers', 7));The has method may also be used to scope properties in order to lessen repetition when asserting against nested properties.
// AssertablePage uses flat dot-notation paths rather than nested scopes.response.AssertInertia(page => page .Has("message") .Has("message.subject") .Count("message.comments", 5)
// Drill deeper with dot-notation including array indexes... .Has("message.comments.0.body") .Count("message.comments.0.files", 1) .Has("message.comments.0.files.0.url"));$response->assertInertia(fn (Assert $page) => $page // Creating a single-level property scope... ->has('message', fn (Assert $page) => $page // We can now continue chaining methods... ->has('subject') ->has('comments', 5)
// And can even create a deeper scope using "dot" notation... ->has('comments.0', fn (Assert $page) => $page ->has('body') ->has('files', 1) ->has('files.0', fn (Assert $page) => $page ->has('url') ) ) ));When scoping into Inertia properties that are arrays or collections, you may also assert that a specified number of items are present in addition to scoping into the first item.
response.AssertInertia(page => page // Assert that there are 5 comments and inspect the first one... .Count("comments", 5) .Has("comments.0.body"));$response->assertInertia(fn (Assert $page) => $page // Assert that there are 5 comments and automatically scope into the first comment... ->has('comments', 5, fn (Assert $page) => $page ->has('body') // ... ));To assert that an Inertia property has an expected value, you may use the where assertion.
response.AssertInertia(page => page // Assert that the subject prop matches the given message... .Where("message.subject", "This is an example message")
// Or, assert against deeply nested values... .Where("message.comments.0.files.0.name", "example-attachment.pdf"));$response->assertInertia(fn (Assert $page) => $page ->has('message', fn (Assert $page) => $page // Assert that the subject prop matches the given message... ->where('subject', 'This is an example message')
// Or, assert against deeply nested values... ->where('comments.0.files.0.name', 'example-attachment.pdf') ));Inertia’s testing methods will automatically fail when you haven’t interacted with at least one of the props in a scope. While this is generally useful, you might run into situations where you’re working with unreliable data (such as from an external feed), or with data that you really don’t want interact with in order to keep your test simple. For these situations, the etc method exists.
// AssertablePage does not enforce interaction, so an equivalent of// etc() is not required — simply assert only the keys you care about.response.AssertInertia(page => page .Has("message.subject") .Has("message.comments"));$response->assertInertia(fn (Assert $page) => $page ->has('message', fn (Assert $page) => $page ->has('subject') ->has('comments') ->etc() ));The missing method is the exact opposite of the has method, ensuring that the property does not exist. This method makes a great companion to the etc method.
response.AssertInertia(page => page .Has("message.subject")
// Assert that the property does not exist... .Missing("message.published_at"));$response->assertInertia(fn (Assert $page) => $page ->has('message', fn (Assert $page) => $page ->has('subject') ->missing('published_at') ->etc() ));Testing Partial Reloads
You may use the reloadOnly and reloadExcept methods to test how your application responds to partial reloads. These methods perform a follow-up request and allow you to make assertions against the response.
// Assert the initial response contains orders but not statuses...var request = InertiaRequest.Get("/orders").Build();var response = await _client.SendAsync(request);
response.AssertInertia(page => page .Has("orders") .Missing("statuses"));
// Then issue a follow-up partial reload that only requests statuses.// Use InertiaRequest.Partial() to set X-Inertia-Partial-Component and// .Only() to set X-Inertia-Partial-Data.var partial = InertiaRequest .Partial("/orders", "Orders/Index") .Only("statuses") .Build();
var reload = await _client.SendAsync(partial);
reload.AssertInertia(page => page .Missing("orders") .Count("statuses", 5));$response->assertInertia(fn (Assert $page) => $page ->has('orders') ->missing('statuses') ->reloadOnly('statuses', fn (Assert $reload) => $reload ->missing('orders') ->has('statuses', 5) ));Instead of passing a single prop as a string, you may also pass an array of props to reloadOnly or reloadExcept.
Testing Deferred Props
You may use the loadDeferredProps method to test how your application responds to deferred props. This method performs a follow-up request to load the deferred properties and allows you to make assertions against the response.
// Deferred props are absent from the initial response...var request = InertiaRequest.Get("/users").Build();var response = await _client.SendAsync(request);
response.AssertInertia(page => page .Has("users") .Has("roles") .Missing("permissions"));
// Load the deferred prop via a follow-up partial reload.var deferred = InertiaRequest .Partial("/users", "Users/Index") .Only("permissions") .Build();
var deferredResponse = await _client.SendAsync(deferred);
deferredResponse.AssertInertia(page => page .Has("permissions") .Where("permissions.0.name", "edit users"));$response->assertInertia(fn (Assert $page) => $page ->has('users') ->has('roles') ->missing('permissions') // Deferred prop not in initial response ->loadDeferredProps(fn (Assert $reload) => $reload ->has('permissions') ->where('permissions.0.name', 'edit users') ));You may also load specific deferred prop groups by passing the group name as the first argument to the loadDeferredProps method.
// InertiaCore does not model deferred prop "groups" directly;// request the specific keys belonging to the group via .Only().var request = InertiaRequest.Get("/users").Build();var response = await _client.SendAsync(request);
response.AssertInertia(page => page .Has("users") .Missing("teams") .Missing("projects"));
var deferred = InertiaRequest .Partial("/users", "Users/Index") .Only("teams", "projects") .Build();
var reload = await _client.SendAsync(deferred);
reload.AssertInertia(page => page .Count("teams", 5) .Has("projects") .Missing("permissions"));$response->assertInertia(fn (Assert $page) => $page ->has('users') ->missing('teams') ->missing('projects') ->loadDeferredProps('attributes', fn (Assert $reload) => $reload ->has('teams', 5) ->has('projects') ->missing('permissions') // Different group ));Instead of passing a single group as a string, you may also pass an array of groups to loadDeferredProps.
// Load the keys from multiple deferred groups in a single partial reload.var deferred = InertiaRequest .Partial("/users", "Users/Index") .Only("permissions", "teams", "projects") .Build();
var reload = await _client.SendAsync(deferred);
reload.AssertInertia(page => page .Has("permissions") .Has("teams") .Has("projects"));$response->assertInertia(fn (Assert $page) => $page ->loadDeferredProps(['default', 'attributes'], fn (Assert $reload) => $reload ->has('permissions') ->has('teams') ->has('projects') ));Testing Flash Data
You may use the hasFlash and missingFlash methods to test flash data in your Inertia responses.
// InertiaCore does not ship dedicated hasFlash helpers; flash data// is surfaced as a shared prop, so assert against it directly.response.AssertInertia(page => page // Assert flash data exists... .Has("flash.message")
// Assert flash data has a specific value... .Where("flash.message", "Item saved!")
// Nested values are supported using "dot" notation... .Where("flash.notification.type", "success")
// Assert flash data does not exist... .Missing("flash.error"));$response->assertInertia(fn (Assert $page) => $page // Assert flash data exists... ->hasFlash('message')
// Assert flash data has a specific value... ->hasFlash('message', 'Item saved!')
// Nested values are supported using "dot" notation... ->hasFlash('notification.type', 'success')
// Assert flash data does not exist... ->missingFlash('error'));Redirect Responses
The hasFlash and missingFlash methods above only work on rendered Inertia page responses. For redirect responses, you may use the assertInertiaFlash and assertInertiaFlashMissing methods directly on the test response to assert against the session’s flash data.
// Inertia redirects return a 302 with a Location header. Follow it// with an Inertia XHR request, then assert against the rendered page// where the flash prop is exposed.var response = await _client.PostAsync("/users", new StringContent("{}"));
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);Assert.Equal("/dashboard", response.Headers.Location?.ToString());
var follow = InertiaRequest .Get(response.Headers.Location!.ToString()) .Build();
var redirectResponse = await _client.SendAsync(follow);
redirectResponse.AssertInertia(page => page .Has("flash.message") .Where("flash.message", "User created!") .Where("flash.notification.type", "success") .Missing("flash.error"));$response = $this->post('/users');
$response->assertRedirect('/dashboard') ->assertInertiaFlash('message') ->assertInertiaFlash('message', 'User created!') ->assertInertiaFlash('notification.type', 'success') ->assertInertiaFlashMissing('error');