Your Pest suite is green and the page still renders blank, or your Vue tests pass while the query is wrong. Each side needs its own test, and neither should pretend to be the other. Here is the split, plus the Vitest setup the starter kit doesn’t include.
TL;DR
- Pest +
assertInertia: routes, validation, queries and the props Inertia receives. - Vitest +
@vue/test-utils: markup that renders from props, with no server. - The kit has no Vitest: install
vitest @vue/test-utils jsdomand addvitest.config.ts. - Don’t mount a page and stub Inertia’s router just to see a heading.
A Laravel and Vue app has two test runs because the two sides fail differently. Pest can prove a POST creates a row and that Inertia received posts.data. It does not mount the Vue page. Vitest can prove a component renders a title from a prop. It does not hit SQLite. The requirements name both Pest and Vitest. This page is how to add the Vue half without pretending the starter kit already has it.
| Pest (PHP) | Vitest (JS) | |
|---|---|---|
| Proves | A POST creates a row; Inertia got posts.data | A component renders a title from a prop |
| Does not | Mount the Vue page | Hit SQLite |
Feature tests first#
The feature tests for create, an empty title, and search are already in the CRUD guide. Run those first.
php artisan test --filter=PostsTestFeature tests use the session stack. They do not need a CSRF token. assertInertia reads the props the controller returned. If posts.data is missing, the controller is still returning a plain array, which is the bug the CRUD page calls out.
A component test#
Heads up: Vitest is not in the kit
Do not claim the kit’s package.json already has Vitest. Add it.
npm install -D vitest @vue/test-utils jsdomimport { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
test: {
environment: "jsdom",
},
});Keep this file next to the kit’s vite.config.ts. A component test should not boot Inertia’s router. Pass props and read the text.
<script setup lang="ts">
defineProps<{ title: string }>();
</script>
<template>
<h2>{{ title }}</h2>
</template>import { mount } from "@vue/test-utils";
import { describe, expect, it } from "vitest";
import PostTitle from "./PostTitle.vue";
describe("PostTitle", () => {
it("renders the title", () => {
const wrapper = mount(PostTitle, {
props: { title: "Hello Laravel 13" },
});
expect(wrapper.text()).toContain("Hello Laravel 13");
});
});{
"scripts": {
"test:unit": "vitest run"
}
}Run the first component test
npm run test:unitGotcha: window at import time
jsdom is there so mount has a document. If the component reads window at import time, the test fails the same way server rendering fails. Move that read into onMounted, or pass the value in as a prop.
What not to mock#
Gotcha: brittle page mocks
Do not mount posts/Index.vue and stub router.get just to see a heading. That test breaks every time Inertia’s API moves, and it never notices a wrong SQL when. The Pest test owns the query. The Vitest test owns markup that has no server.
Typed props for the page itself are TypeScript on Vue pages. The stack you are testing is Inertia, a SPA, or Nuxt.
Recap#
- Two runners, two jobs: Pest for the server, Vitest for components.
- Add Vitest yourself; the kit does not ship it.
- Component tests pass props, not routers.
- Keep queries tested in Pest.
FAQ#
Does the Laravel Vue starter kit include Vitest?
No. It ships Pest for PHP. Install vitest, @vue/test-utils and jsdom to test Vue components.
How do I test Inertia props in Laravel?
Use assertInertia in a Pest feature test to check the component name and props like posts.data.
Should I mount Inertia pages in Vitest?
Usually not. Mount small components with props. Pages depend on Inertia’s router, and their data is better tested in Pest.
Next steps#
- CRUD with Inertia and Vue
- TypeScript props for Laravel + Vue pages
- Server-side rendering with Inertia



