Quick Vitest Setup With ViteJS, React, & TypeScript
Avoid Spending Time Setting Up Vitest For Your Projects & Follow This Quick Setup
What Is Vitest?
“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”
- https://vitest.dev/guide/why.html
What does complex transformations mean? It means that you as a developer don’t need to fiddle around with Jest configurations like the following to get simple things working to support different types of React components.
Example Jest Configuration: jest.config.ts
export default {
testEnvironment: "jsdom",
transform: {
"^.+\\.tsx?$": "ts-jest"
},
moduleNameMapper: {
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js',
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
What We’re Building
Simple walkthrough to get Vitest to get it up and running with your ViteJS React TypeScript application.
This project extends the scaffolded React TypeScript Template from ViteJS. If you ever wanted to not waste time, then follow these steps to get things setup.
TL;DR
In case you want to skip the explanation and just get straight to the code, scroll down to Final Code 👇.
Requirements
Make sure you have the following setup on your computer before going forward with the walkthrough.
- NVM or Node
v18.12.1
LTS - Pnpm
v7.15.0
*
⚠️ *NOTE: there are some additional configurations to be made with Pnpm.
Project Setup Walkthrough
Let’s start by scaffolding out our React TypeScript app with ViteJS.
# FROM ./path/to/your/project
pnpm create vite;
# Expected Prompts:
# ✔ Project name: … quick-vitejs-react-typescript-vitest
# ✔ Select a framework: › React
# ✔ Select a variant: › TypeScript
cd quick-vitejs-react-typescript-vitest;
Our Scaffolded ViteJS React TypeScript Application
Let’s take a look at what we’re working with for our base application.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm install;
pnpm dev;
# Expected Output:
# VITE v4.0.3 ready in 703 ms
#
# ➜ Local: http://127.0.0.1:5173/
# ➜ Network: use --host to expose
# ➜ press h to show help
Installing & Configure Vitest
Next, let’s install Vitest and set it up so that our Vite configuration works with it.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D vitest;
Update our Vite configuration file.
File: ./vite.config.ts
# BEFORE
# import { defineConfig } from 'vite'
# AFTER
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
Writing Our First Test
You can add your test files anywhere you’d like, but I prefer to create a separate folder that is dedicated to it that matches the directory of the file I’m trying to test to easily find it and not clog up my file directory with a bunch of test files.
# FROM ./quick-vitejs-react-typescript-vitest
mkdir src/__tests__;
touch src/__tests__/App.test.tsx
Our first test will be to ensure that the tests are working correctly.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect } from 'vitest';
// Tests
describe('Renders main page correctly', async () => {
it('Should render the page correctly', async () => {
expect(true).toBeTruthy();
});
});
Now we run Vitest to perform the tests we setup.
# FROM ./quick-vitejs-react-typescript-vitest
./node_modules/.bin/vitest;
# Expected Output:
# DEV v0.26.2 /path/to/quick-vitejs-react-typescript-vitest
#
# ✓ src/__tests__/App.test.tsx (1)
#
# Test Files 1 passed (1)
# Tests 1 passed (1)
# Start at 12:01:28
# Duration 524ms (transform 229ms, setup 0ms, collect 14ms, tests 2ms)
#
#
# PASS Waiting for file changes...
# press h to show help, press q to quit
We’ll make this a bit easier on ourselves by modifying our package.json
to run Vitest with pnpm run test
.
File: ./package.json
{
"name": "quick-vitejs-react-typescript-vitest",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^3.0.0",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vitest": "^0.26.2"
}
}
Adding React Support
You’ll notice that we aren’t testing any React components yet and that’s because we need to add support for this with @testing-library/react.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D @testing-library/react;
We’ll modify our test file to run our App.tsx
component and check if the words “Vite + React” are rendered correctly on the page.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
// To Test
import App from '../App';
// Tests
describe('Renders main page correctly', async () => {
it('Should render the page correctly', async () => {
// Setup
render(<App />);
const h1 = await screen.queryByText('Vite + React');
// Expectations
expect(h1).not.toBeNull();
});
});
If we run this, we should get an error with document is not defined
.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# FAIL src/__tests__/App.test.tsx > Renders main page correctly > Should render the page correctly
# ReferenceError: document is not defined
# ...
# ❯ src/__tests__/App.test.tsx:12:9
# 10| it('Should render the page correctly', async () => {
# 11| // Setup
# 12| render(<App />);
# | ^
This is because the tests don’t know what document
is as a global object and in order to support it we need to add an additional library.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D jsdom;
In order to take advantage of this library, we need to change our vite.config.ts
file.
File: ./vite.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom'
}
})
Now if we run our tests again, we should see our tets passing.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# DEV v0.26.2 /path/to/quick-vitejs-react-typescript-vitest
#
# ✓ src/__tests__/App.test.tsx (1)
#
# Test Files 1 passed (1)
# Tests 1 passed (1)
# Start at 12:22:44
# Duration 877ms (transform 245ms, setup 0ms, collect 153ms, tests 17ms)
And if we double check by changing the test to “NOT FOUND Vite + React”, we should see our tests fail.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# FAIL src/__tests__/App.test.tsx > Renders main page correctly > Should render the page correctly
#AssertionError: expected null not to be null
# ❯ src/__tests__/App.test.tsx:16:24
# 14|
# 15| // Expectations
# 16| expect(h1).not.toBeNull();
# | ^
# 17| });
# 18| });
Testing Events
To test if the button has been clicked a certain amount of times and the UI is reflecting those changes, we’re going to take advantage of fireEvent
.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
// To Test
import App from '../App';
// Tests
describe('Renders main page correctly', async () => {
/**
* Passes - shows title correctly
*/
it('Should render the page correctly', async () => {
// Setup
await render(<App />);
const h1 = await screen.queryByText('Vite + React');
// Post Expectations
expect(h1).not.toBeNull();
});
/**
* Passes - shows the button count correctly present
*/
it('Should show the button count set to 0', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Expectations
expect(button).not.toBeNull();
});
/**
* Passes - clicks the button 3 times and shows the correct count
*/
it('Should show the button count set to 3', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Pre Expectations
expect(button).not.toBeNull();
// Actions
fireEvent.click(button as HTMLElement);
fireEvent.click(button as HTMLElement);
fireEvent.click(button as HTMLElement);
// Post Expectations
expect(button?.innerHTML).toBe('count is 3');
});
});
If we run this, we’ll see that it fails, and reason is because we have 3 different renders and it naturally spills over into the next test, so we have multiple elements.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# DEV v0.26.2 /path/to/quick-vitejs-react-typescript-vitest
#
# ❯ src/__tests__/App.test.tsx (3)
# ❯ Renders main page correctly (3)
# ✓ Should render the page correctly
# × Should show the button count set to 0
# × Should show the button count set to 3
#
# ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
#
# FAIL src/__tests__/App.test.tsx > Renders main page correctly > Should show the button count set to 0
# TestingLibraryElementError: Found multiple elements with the text: count is 0
To fix this, we’re going to invoke cleanup
and make sure that it’s run after each test with afterEach
.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
// To Test
import App from '../App';
// Tests
describe('Renders main page correctly', async () => {
/**
* Resets all renders after each test
*/
afterEach(() => {
cleanup();
});
/**
* Passes - shows title correctly
*/
it('Should render the page correctly', async () => {
// Setup
await render(<App />);
const h1 = await screen.queryByText('Vite + React');
// Post Expectations
expect(h1).not.toBeNull();
});
/**
* Passes - shows the button count correctly present
*/
it('Should show the button count set to 0', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Expectations
expect(button).not.toBeNull();
});
/**
* Passes - clicks the button 3 times and shows the correct count
*/
it('Should show the button count set to 3', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Pre Expectations
expect(button).not.toBeNull();
// Actions
fireEvent.click(button as HTMLElement);
fireEvent.click(button as HTMLElement);
fireEvent.click(button as HTMLElement);
// Post Expectations
expect(button?.innerHTML).toBe('count is 3');
});
});
We should now see all our tests passing.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output
# DEV v0.26.2 /path/to/quick-vitejs-react-typescript-vitest
#
# ✓ src/__tests__/App.test.tsx (3)
#
# Test Files 1 passed (1)
# Tests 3 passed (3)
# Start at 13:21:28
# Duration 931ms (transform 260ms, setup 0ms, collect 173ms, tests 31ms)
#
#
# PASS Waiting for file changes...
# press h to show help, press q to quit
Replacing FireEvent
One of things we’ll want to change up is replacing fireEvent
with user events
.
“
fireEvent
dispatches DOM events, whereasuser-event
simulates full interactions, which may fire multiple events and do additional checks along the way.”
- https://testing-library.com/docs/user-event/intro
To start, we’ll need to install this new library.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D @testing-library/user-event;
Modifying our App.test.tsx
to take advantage of user event.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// To Test
import App from '../App';
// Tests
describe('Renders main page correctly', async () => {
/**
* Resets all renders after each test
*/
afterEach(() => {
cleanup();
});
/**
* Passes - shows title correctly
*/
it('Should render the page correctly', async () => {
// Setup
await render(<App />);
const h1 = await screen.queryByText('Vite + React');
// Post Expectations
expect(h1).not.toBeNull();
});
/**
* Passes - shows the button count correctly present
*/
it('Should show the button count set to 0', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Expectations
expect(button).not.toBeNull();
});
/**
* Passes - clicks the button 3 times and shows the correct count
*/
it('Should show the button count set to 3', async () => {
// Setup
const user = userEvent.setup();
await render(<App />);
const button = await screen.queryByText('count is 0');
// Pre Expectations
expect(button).not.toBeNull();
// Actions
await user.click(button as HTMLElement);
await user.click(button as HTMLElement);
await user.click(button as HTMLElement);
// Post Expectations
expect(button?.innerHTML).toBe('count is 3');
});
});
We should see that all our tests pass like before.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# DEV v0.26.2 /path/to/quick-vitejs-react-typescript-vitest
#
# ✓ src/__tests__/App.test.tsx (3)
#
# Test Files 1 passed (1)
# Tests 3 passed (3)
# Start at 13:34:24
# Duration 1.02s (transform 244ms, setup 0ms, collect 219ms, tests 69ms)
#
#
# PASS Waiting for file changes...
# press h to show help, press q to quit
Extending Vitest With Jest Dom
Seeing our tests, we might want to make our tests a bit more clear than just an element being equal or not equal to null.
// Before
expect(h1).not.toBeNull();
// Better
expect(h1).toBeInTheDocument();
We can leverage @testing-library/jest-dom to take advantage of clearer tests and additional functionality.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D @testing-library/jest-dom;
We’ll modify our file to include this new convention.
File: ./src/__tests___/App.test.tsx
// Imports
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// To Test
import App from '../App';
// Tests
describe('Renders main page correctly', async () => {
/**
* Resets all renders after each test
*/
afterEach(() => {
cleanup();
});
/**
* Passes - shows title correctly
*/
it('Should render the page correctly', async () => {
// Setup
await render(<App />);
const h1 = await screen.queryByText('Vite + React');
// Post Expectations
expect(h1).toBeInTheDocument();
});
/**
* Passes - shows the button count correctly present
*/
it('Should show the button count set to 0', async () => {
// Setup
await render(<App />);
const button = await screen.queryByText('count is 0');
// Expectations
expect(button).toBeInTheDocument();
});
/**
* Passes - clicks the button 3 times and shows the correct count
*/
it('Should show the button count set to 3', async () => {
// Setup
const user = userEvent.setup();
await render(<App />);
const button = await screen.queryByText('count is 0');
// Pre Expectations
expect(button).toBeInTheDocument();
// Actions
await user.click(button as HTMLElement);
await user.click(button as HTMLElement);
await user.click(button as HTMLElement);
// Post Expectations
expect(button?.innerHTML).toBe('count is 3');
});
});
In order to get it to work, we need to add an additional configuration to our Vitest with a setup file.
# FROM ./quick-vitejs-react-typescript-vitest
touch ./vitest.setup.ts;
File: ./vitest.setup.ts
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
Configure our Vite config file to point to this setup.
File: ./vite.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts']
}
})
If we run our tests, we should see it all passing.
# FROM ./quick-vitejs-react-typescript-vitest
pnpm test;
# Expected Output:
# DEV v0.26.2 /Users/manny/Documents/github/quick-vitejs-react-typescript-vitest
#
# ✓ src/__tests__/App.test.tsx (3)
#
# Test Files 1 passed (1)
# Tests 3 passed (3)
# Start at 13:45:39
# Duration 1.12s (transform 255ms, setup 110ms, collect 158ms, tests 82ms)
#
#
# PASS Waiting for file changes...
# press h to show help, press q to quit
That’s great, except we have one problem with our type definitions not showing up for our test file.
# FROM ./quick-vitejs-react-typescript-vitest
./node_modules/.bin/tsc;
# Expected Output:
# src/__tests__/App.test.tsx:27:20 - error TS2339: Property 'toBeInTheDocument' does not exist on type 'Assertion<HTMLElement | null>'.
#
# 27 expect(h1).toBeInTheDocument();
# ~~~~~~~~~~~~~~~~~
#
# src/__tests__/App.test.tsx:39:24 - error TS2339: Property 'toBeInTheDocument' does not exist on type 'Assertion<HTMLElement | null>'.
#
# 39 expect(button).toBeInTheDocument();
# ~~~~~~~~~~~~~~~~~
#
# src/__tests__/App.test.tsx:52:24 - error TS2339: Property 'toBeInTheDocument' does not exist on type 'Assertion<HTMLElement | null>'.
#
# 52 expect(button).toBeInTheDocument();
# ~~~~~~~~~~~~~~~~~
#
#
# Found 3 errors in the same file, starting at: src/__tests__/App.test.tsx:27
This is something that is a known issue with pnpm currently and there is a fix for it.
To fix this, we just need to install another dependency for the type definitions
# FROM ./quick-vitejs-react-typescript-vitest
pnpm add -D @types/testing-library__jest-dom;
If try testing our types again, we’ll see that everything is working as expected.
# FROM ./quick-vitejs-react-typescript-vitest
./node_modules/.bin/tsc;
# Expeted Output:
# (Blank - No Errors)
With that we have our base tests setup to work with Vitest.
Final Code
Here is the file code if you’d like to take a look.
What’s Next?
If you prefer using Jest, then definitely check out Quick Jest Setup With ViteJS, React, & TypeScript.
My hope it to show some additional tests that are a bit more complicated with states and routes, so definitely make sure Follow me to keep up to date.
If you got value from this, please share this and also follow me on twitter (where I’m quite active) @codingwithmanny and instagram at @codingwithmanny.