Unit Testing React Code in Laravel Using Vitest
I am working on a project that makes use React within Laravel.
I didn’t find much support for setting up React tests.
So here are som notes on how I got it working.
Quick Recap
Laravel and Inertia are glued together using Inertia which works really well.
I get the full power of Laravel’s database abstraction and the user experience of a React App.
Laravel provides an API and Inertia intercepts requests in the client to provide seamless page updates.
It’s not quite like having a Singe Page App - but it means all my frontend code is in a form my JS frontend colleagues can collaborate with while all the backend code is really quick to work on.
There is great test support for the PHP side of the API via Inertia\Testing\AssertableInertia
But the Inertia docs on testing just say
Another approach to testing your page components is using a client-side unit testing framework, such as Jest or Mocha.
Javascript Quantumania
The biggest problem I have when working in Javascript is that you can be writing plan JS or Typescript, compiling with Webpack or Vite, using React or Vue… and then you have all the versions of all the things.
Despite trying to follow what seems like a common path you end up needing a specific set of technologies that nobody seems to be writing about.
I’m using Laravel - which defaults to Vite for asset bundling.
I use React - which seems to mostly use Jest for tests.
Jest is not fully supported by vite
Vitest
Using Vite dev server to transform your files during testing, enables the creation of a simple runner that doesn’t need to deal with the complexity of transforming source files and can solely focus on providing the best DX during testing.
So this sounds good, and at the time of writing Vitest seems to be stable enough and widely adopted enough to work - but it is still a little niche and I couldn’t find clear docs on how to get started
Here is what I did find
My Config
I added the following to package.json
npm install --save-dev @testing-library/jest-dom @testing-library/react @testing-library/user-event jsdom @vitejs/plugin-react vitest
Added these lines to vite.config.js
test: {
environment: 'jsdom',
globals: true,
resetMocks: true,
},
resolve: {
alias: {
'@': '/resources/js',
},
},
First Test
import { render, screen } from '@testing-library/react'
import { vi, describe, it, expect } from 'vitest'
import Country from '@/Pages/Accounts/Country';
// Mock the Inertia route function
global.route = vitest.fn();
// Mock AuthenticatedLayout since we don't need to test it
vi.mock('@/Layouts/AuthenticatedLayout', () => ({
default: ({ children }) => <div data-testid="auth-layout">{children}</div>,
}));
// Mock Inertia components
vi.mock('@inertiajs/react', () => ({
Head: ({ children }) => <div data-testid="head">{children}</div>,
Link : ({ children }) => <div data-testid="link">{children}</div>
}));
describe('Country Component', () => {
const defaultProps = {
auth: {
user: { name: 'Test User' }
},
site: { id: 1, name: 'Test Site' },
countries: [
{ country_id: 1, id: 1, name: 'United States', total: 100 },
{ country_id: 2, id: 2, name: 'Canada', total: 50 },
],
total: 150,
};
it('renders without occupation filter', () => {
render(<Country {...defaultProps} />);
expect(screen.getByText('All Occupations By Country'));
expect(screen.getByText('Total Accounts: 150'));
});
it('renders with occupation filter', () => {
const propsWithOccupation = {
...defaultProps,
occupation: { name: 'Developer' },
};
render(<Country {...propsWithOccupation} />);
expect(screen.getByText('Developer By Country'));
expect(screen.queryByText('Total Accounts:')).not;
});
it('renders with occupation and other filter', () => {
const propsWithOccupationAndOther = {
...defaultProps,
occupation: { name: 'Developer' },
other: 'Frontend',
};
render(<Country {...propsWithOccupationAndOther} />);
expect(screen.getByText('Developer (Frontend) By Country'));
});
it('renders country table with correct data', () => {
render(<Country {...defaultProps} />);
expect(screen.getByText('United States'));
expect(screen.getByText('100'));
expect(screen.getByText('Canada'));
expect(screen.getByText('50'));
});
it('handles unknown country names', () => {
const propsWithUnknownCountry = {
...defaultProps,
countries: [
{ country_id: 1, id: 1, name: null, total: 100 },
],
};
render(<Country {...propsWithUnknownCountry} />);
expect(screen.getByText('Unknown'));
});
it('handles empty props', () => {
render(<Country />);
expect(screen.getByText('All Occupations By Country'));
});
});
Notes
Once I had the right config in place I started to get failing tests - which was progress.
But they were failing because the test running couldn’t find components within Inertia
Mocking those components solved my problems and after that any failing tests were real issues with my code