Quick Vitest Setup With ViteJS, React, & TypeScript

Avoid Spending Time Setting Up Vitest For Your Projects & Follow This Quick Setup

Manny
12 min readDec 27, 2022
A Walkthrough On How To Setup Vitest Quickly With ViteJS, React, & TypeScript

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

https://vitest.dev

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
ViteJS Scaffolded React TypeScript Application

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, whereas user-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.

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.

--

--

Manny

DevRel Engineer @ Berachain | Prev Polygon | Ankr & Web Application / Full Stack Developer