
Modern web applications rely on JavaScript and frameworks like React to deliver powerful user experiences. However, as applications get more and more complex, reliably testing them gets increasingly challenging. This is why an automated suite of robust tests is crucial for the long-term success of any application.
In this post, we’ll explore how we use state-of-the-art tools to ensure the stability and reliability of our React applications. We’ll cover how to unit test a React application using a modern toolkit: Vitest for fast test execution, MSW (Mock Service Worker) for mocking APIs and Playwright for web browser rendering.
Checkout the full code here: https://github.com/makepath/react-vitest-examples/
Why Test React Apps?
Unit testing verifies that all individual parts of an application (its components, functions, and business logic) work as expected. It catches regressions early, improves code quality, and acts as documentation for your codebase. Combined with integration and end-to-end tests, unit testing ensures your app behaves as expected across all layers.
The Tools
- Vitest: A blazing-fast test runner with Jest compatibility, optimized for Vite projects.
- Playwright: A powerful tool for browser automation, supporting both component and end-to-end testing.
- MSW: A library for mocking HTTP requests via Service Workers, perfect for testing API interactions.
- Istanbul: A code coverage tool that helps identify untested parts of your codebase.
1. Vitest Configuration
Vitest provides a fast and efficient way to test React applications. It provides the basic infrastructure for our frontend testing framework. We combine it with Istanbul for code coverage and Playwright for testing how components behave in a real browser environment.
// vitest.config.ts
/// <reference types="@vitest/browser/providers/playwright" />
import { defineConfig, coverageConfigDefaults } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
root: './src',
coverage: {
provider: 'istanbul',
},
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }],
},
},
})// package.json
{
...
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest --coverage"
},
...
}2. Unit Testing Components with Vitest
Vitest integrates seamlessly with Vite and supports modern features like ES modules and TypeScript, making it an ideal choice for unit testing React components.
Example: Testing a Simple Component
Let’s test a simple Counter component that increments a value on button click:
// Counter.tsx
import { useState } from 'react'
export const Counter = () => {
const [count, setCount] = useState(0)
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
}Create a test file with Vitest and React Testing Library:
// Counter.test.tsx
import { render } from 'vitest-browser-react'
import { userEvent } from '@vitest/browser/context'
import { describe, it, expect } from 'vitest'
import { Counter } from './Counter'
describe('Counter component', () => {
it('Counter increments on click', async () => {
const { container } = render(<Counter />)
const button = container.getByText('Count: 0')
await userEvent.click(button)
expect(button.textContent).toBe('Count: 1')
})
})Run tests with npm run test and checkout the code coverage with npm run coverage.
3. Mocking APIs with MSW
A lot of React components require communication with a backend API. Instead of running the full API, MSW allows us to mock API responses easily. MSW intercepts HTTP requests and replies with data we can control.
Step 1: Define Mock API Handlers
Create a handlers.ts file to define a mock endpoints with specific data. In the example below, we mock a user profile endpoint that returns John Doe as the name:
// handlers.ts
import { HttpResponse, http } from 'msw'
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({ name: 'John Doe' })
}),
]Step 2: Integrate MSW with Vitest
Set up MSW in your test environment:
// setupTests.ts
import { afterAll, afterEach, beforeAll } from 'vitest'
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())Update the Vitest configuration to include the MSW configuration:
// vitest.config.ts
export default defineConfig({
...
test: {
...
setupFiles: './src/setupTests.ts', // for global test setup like MSW
...
},
...
})Step 3: Test a Component with API Calls
Test a UserProfile component that fetches data, using the mocked response data as the expected output:
// UserProfile.test.tsx
import { render } from 'vitest-browser-react'
import { expect, test, vi } from 'vitest'
import UserProfile from './UserProfile'
test('Displays user name', async () => {
const { container } = render(<UserProfile />)
await vi.waitFor(() => {
expect(container.getByText('John Doe')).toBeInTheDocument()
})
})4. Testing Redux Toolkit State Logic
Since Redux actions and selectors are simple functions, it’s pretty easy to unit test all your state logic. Let’s checkout some examples.
Example: Testing a Redux Slice
Assume a counterSlice with actions and selectors:
// counterSlice.ts
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 0
},
reducers: {
increment: (state) => state.count + 1,
},
selectors: {
selectCount: (state) => state.count),
},
});
export const { increment } = counterSlice.actions;
export const { selectCount } = counterSlice.selectors;
export default counterSlice.reducer;Write tests for the slice:
// counterSlice.test.ts
import { describe, it, expect, vi } from 'vitest'
import reducer, { increment, selectCount } from './counterSlice'
describe('Counter Slice', () => {
const initialState = {
count: 0,
}
beforeEach(() => {
vi.clearAllMocks()
})
it('increment count', () => {
const state = reducer(initialState, increment())
expect(state.count).toBe(1)
})
it('should select count', () => {
const mockState = {
counter: {
count: 42,
},
}
const count = selectCount(mockState)
expect(count).toEqual(42)
})
})Testing Async Thunks with MSW
For async actions, combine Redux Toolkit and MSW:
// userThunk.test.ts
import { expect, test } from 'vitest'
import { setupStore } from './store'
import { fetchUser } from './userThunk'
import { handlers } from '../mocks/handlers'
const server = setupServer(...handlers)
test('fetchUser thunk', async () => {
const store = setupStore()
await store.dispatch(fetchUser())
expect(store.getState().user.name).toBe('John Doe')
})Summary: Best Practices
- Isolate Tests: Mock external dependencies (APIs, state) to focus on units.
- Test User Flows: Use Playwright for critical user journeys (e.g., login, checkout).
- Leverage TypeScript: Catch errors early with type-checking in tests.
- Parallelize: Vitest runs tests in parallel by default; use this to speed up suites.
Ready for the Next Step?
Robust testing suites are just one building block we use to transform your idea into beautiful, future-proof web applications.
Want to know more? Contact us today to find out how we can help you build and scale your web application to make your vision shine!
Popular
Machine Learning for Change Detection: Part 1
GPU-Enhanced Geospatial Analysis
Open Source Machine Learning Tools (Updated for 2023)
Getting Started with Open Source (Updated for 2023)
The History of Open Source GIS: An Interactive Infographic (Updated for 2023)
Superpowered GIS: ESRI’s ArcGIS + Open Source Spatial Analysis Tools
Seniors at Risk: Using Spatial Analysis to Identify Pharmacy Deserts
Open Source Spatial Analysis Tools for Python: A Quick Guide (Updated for 2022)



