Vitest is a lightweight and fast testing framework built on Vite, designed to integrate with modern JavaScript and TypeScript projects smoothly.
With Vitest, you benefit from first-class ES module support, instant test execution, and a familiar Jest-compatible API, making it an excellent choice for front-end and back-end applications.
In this article, you'll explore Vitest's core features and learn how to write and execute tests efficiently.
Prerequisites
Before proceeding with this tutorial, ensure you have Node.js installed, preferably the latest LTS version.
Additionally, this guide assumes familiarity with JavaScript and a basic understanding of testing concepts.
Step 1 — Setting up the directory
In this section, you'll create a project directory and set up Vitest to test your JavaScript code.
To begin, create a new directory and navigate into it:
mkdir vitest-demo && cd vitest-demo
Then, initialize the directory as an npm project:
npm init -y
This command creates a package.json
file, which stores metadata and dependencies for your project.
Next, enable ES Modules by adding the following to your package.json
file:
npm pkg set type="module"
Now, install Vitest as a development dependency:
npm install -D vitest
After installation, add a test script to your package.json
file:
{
. . .
"scripts": {
"test": "vitest"
}
}
You're now ready to create a simple ES Module function and test it using Vitest. Here’s the function in full:
export function add(a, b) {
return a + b;
}
The add()
function takes two numbers and returns their sum. Save this file at the root of your project.
Next, let’s write the first test using Vitest.
Step 2 — Writing your first test
Unit tests help ensure that individual functions behave as expected under different conditions. Instead of manually verifying outputs, you will automate the process using Vitest.
Start by creating a tests
directory in your project root:
mkdir tests
Next, create a file named math.test.js
inside the tests
directory and add the following test:
import { describe, it, expect } from "vitest";
import { add } from "../math.js";
describe("add function", () => {
it("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3);
});
});
The describe
function organizes related tests within a test suite, providing a clear structure to the test file.
Within this suite, the it
function defines individual test cases, each specifying a particular behavior to verify.
To ensure correctness, the expect
function performs assertions, checking whether add(1, 2)
returns 3
as expected.
Now that the test is written, let's run it in the next step.
Step 3 — Running your tests
With your test file set up, you can run it using Vitest.
To execute all tests, run:
npm test
Since the test script in package.json
is set to vitest
, this command is equivalent to:
npx vitest
Vitest will automatically find and run test files inside the tests
directory. The output should look something like this:
✓ tests/math.test.js (1 test) 1ms
✓ add function > should return 3 when adding 1 and 2
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 10:42:21
Duration 228ms (transform 18ms, setup 0ms, collect 14ms, tests 1ms, environment 0ms, prepare 40ms)
PASS Waiting for file changes...
press h to show help, press q to quit
The test runner successfully detected and executed the math.test.js
file, confirming that the test suite ran as expected. Alongside the results, the execution time for each test case is displayed, providing insight into the performance of the test run.
Vitest also enables watch mode by default, as indicated by the PASS Waiting for file changes...
line in the output. Once tests have run, Vitest remains active, continuously monitoring for modifications.
Any changes to math.js
or math.test.js
trigger an automatic re-run, eliminating the need to restart the process manually. This creates a fast feedback loop, making it easier to catch errors and verify updates in real-time.
Step 4 — Test filtering and running specific tests
As your test suite grows, it becomes increasingly important to run only relevant tests. Vitest provides several ways to filter and run specific tests, making the development process faster and more efficient.
Using .only
to focus on specific tests
If you want to run just one test or a group of tests within a file, you can use the .only
method. This will execute only the test(s) marked with .only
, allowing you to focus on specific functionality during development.
For example, if you're working on a feature and only want to run the add
function test, modify the test like this:
describe("add function", () => {
it.only("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3);
});
it("should return 5 when adding 2 and 3", () => {
expect(add(2, 3)).toBe(5);
});
});
When you run the tests, only the test with .only
will execute, and the other test will be skipped. Here's how the output would look:
✓ tests/math.test.js (2 tests | 1 skipped) 1ms
✓ add function > should return 3 when adding 1 and 2
↓ add function > should return 5 when adding 2 and 3
Test Files 1 passed (1)
Tests 1 passed | 1 skipped (2)
Start at 11:10:58
Duration 7ms
PASS Waiting for file changes...
press h to show help, press q to quit
As shown in the output above, the first test ran successfully, while the second one was skipped because it wasn’t marked with .only
.
This allows you to zero in on the specific functionality you're working on without running the entire suite.
Using .skip
to temporarily exclude tests
Sometimes, you may want to temporarily exclude certain tests, like when they're not ready or are causing issues. You can do this with .skip
, which will skip the marked test during the run.
For instance, if you want to skip the first test, you can change it to .skip
like this:
...
describe("add function", () => {
it.skip("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3);
});
it("should return 5 when adding 2 and 3", () => {
expect(add(2, 3)).toBe(5);
});
});
In this case, when you run the tests, the first test will be skipped, but the second will run:
✓ tests/math.test.js (2 tests | 1 skipped) 1ms
↓ add function > should return 3 when adding 1 and 2
✓ add function > should return 5 when adding 2 and 3
Test Files 1 passed (1)
Tests 1 passed | 1 skipped (2)
Start at 11:32:06
Duration 8ms
PASS Waiting for file changes...
press h to show help, press q to quit
As shown in the output above, the test with .skip
is excluded from the run, allowing you to focus on the tests that are relevant to your current work.
Filtering tests with command line arguments
Vitest provides powerful command-line filtering options that let you selectively run tests without modifying your code. These options are especially useful in CI/CD pipelines or when you need to focus on specific test areas.
For example, let's say you want to run all tests related to the add
function. Here's the original test suite without any filtering:
...
describe("add function", () => {
it("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3);
});
it("should return 5 when adding 2 and 3", () => {
expect(add(2, 3)).toBe(5);
});
});
You can filter tests based on their description using the -t
(or --testNamePattern
) flag. This allows you to run only tests that match a specific pattern in their description, which is perfect for isolating functionality while working on a particular feature or debugging an issue.
To run the tests that mention the add function
in their description, use the following command:
npm test -- -t "add function"
The double dash --
separates npm’s arguments from those passed to Vitest. This command runs only tests that include the string "add function"
in their descriptions. The output should look like this:
✓ tests/math.test.js (2 tests) 1ms
✓ add function > should return 3 when adding 1 and 2
✓ add function > should return 5 when adding 2 and 3
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 12:04:37
Duration 5ms
PASS Waiting for file changes...
press h to show help, press q to quit
In this example, only the tests related to the add function
are run, and the output confirms that both tests passed.
You can further narrow your test selection by using more specific patterns. For example, if you want to run only the tests that contain the phrase "should return 3"
, you can do so like this:
npm test -- -t "should return 3"
✓ tests/math.test.js (2 tests | 1 skipped) 1ms
✓ add function > should return 3 when adding 1 and 2
↓ add function > should return 5 when adding 2 and 3
Test Files 1 passed (1)
Tests 1 passed | 1 skipped (2)
Start at 12:07:20
Duration 227ms (transform 13ms, setup 0ms, collect 11ms, tests 1ms, environment 0ms, prepare 40ms)
PASS Waiting for file changes...
press h to show help, press q to quit
This will run only the test containing "should return 3"
in its description.
Step 5 — Using in-source tests
Vitest makes writing tests directly within your source code easy, eliminating the need for separate test files.
This approach, known as in-source testing, is useful when you want to quickly test individual functions without the overhead of creating and managing separate test files.
For example, take the math.js
file that contains the add()
function. You can modify it to include in-source tests by adding the following code:
import { it, expect } from 'vitest';
export function add(a, b) {
return a + b;
}
// In-source tests (only run during testing)
if (import.meta.env.MODE === 'test') {
it("should return 3 when adding 1 and 2", () => {
expect(add(1, 2)).toBe(3);
});
it("should return 5 when adding 2 and 3", () => {
expect(add(2, 3)).toBe(5);
});
}
Here, the tests are added directly below the function. When you run the tests, Vitest detects these in-source tests and runs them, just like it would run any test file.
The import.meta.env.MODE === 'test'
condition ensures that the tests are only executed during test runs, preventing them from affecting production builds.
Since the test logic is embedded within the same file as the source code, Vitest runs these in-source tests along with any other tests defined in your project. The output will look similar to the following:
✓ tests/math.test.js (4 tests) 2ms
✓ should return 3 when adding 1 and 2
✓ should return 5 when adding 2 and 3
✓ add function > should return 3 when adding 1 and 2
✓ add function > should return 5 when adding 2 and 3
Test Files 1 passed (1)
Tests 4 passed (4)
Start at 12:17:22
Duration 231ms (transform 24ms, setup 0ms, collect 18ms, tests 2ms, environment 0ms, prepare 39ms)
PASS Waiting for file changes...
press h to show help, press q to quit
The output shows that Vitest executes both the standard test file we created in Step 2 and the in-source tests.
While this method is convenient, especially in small applications, separating your tests from the source code is generally recommended as your project grows. This separation improves maintainability and scalability, especially when working in teams or on larger codebases.
With that in mind, we’ll stick to the traditional approach of keeping the tests separate from the source code. Here's the original math.js
function again:
export function add(a, b) {
return a + b;
}
Now that you’ve learned how to write and execute basic in-source tests, you will learn mocking.
Step 6 — Mocking with Vitest
Mocking allows you to simulate external dependencies, making isolating the functionality you're testing easier. This is particularly helpful when avoiding slow or unreliable systems like file systems, databases, or APIs in your tests.
This section will walk you through an example of mocking the fs.readFile
method using Vitest’s built-in mocking features. The goal is to mock the behavior of reading from a file without actually interacting with the filesystem.
Let’s say you have a function that reads the contents of a text file. Instead of reading the file during the test, which could be time-consuming or unreliable, you can mock fs.readFile
to return a fixed value. This makes your tests faster and more controlled.
Start by creating an empty file, text-content.txt
:
touch text-content.txt
Then, create a fileReader.js
file in the root of the project directory with the following function to read the contents of a file:
import { readFile } from "fs/promises";
export async function readFileContent(filePath) {
try {
const content = await readFile(filePath, "utf-8");
return content.trim();
} catch (error) {
throw new Error(`Failed to read file: ${error.message}`);
}
}
This function uses fs.promises.readFile
to asynchronously read the contents of a file and returns the content as a string.
Next, create a test file called fileReader.test.js
inside the tests
directory. The goal here is to mock fs.promises.readFile
so it returns a predefined string instead of actually reading from the filesystem.
import { describe, it, expect, vi } from "vitest";
import { readFileContent } from "../fileReader.js";
import * as fs from "fs/promises";
// Use vi.mock with proper implementation
vi.mock("fs/promises", async () => {
return {
readFile: vi.fn().mockResolvedValue("Mocked content"),
};
});
describe("readFileContent function", () => {
it("should successfully read the content of a text file", async () => {
const content = await readFileContent("text-content.txt");
// Check that fs.readFile was called once
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith("text-content.txt", "utf-8");
// Ensure the content matches the mocked value
expect(content).toBe("Mocked content");
});
});
Here, the code imports the necessary testing utilities and the readFileContent function being tested. The important part is where vi.mock()
replaces the actual fs/promises
module with a mock implementation containing a simulated readFile
function that returns "Mocked content" instead of reading from the actual file system.
When the test calls readFileContent()
, it unknowingly uses this mock instead of the real file system API. The test then verifies that the mock was called exactly once with the correct arguments, and that the function returned the expected mocked content.
This approach isolates the test from the actual file system, making it faster, more predictable, and free from external dependencies—a key practice in unit testing.
Upon saving, the output should look something like this:
✓ tests/fileReader.test.js (1 test) 2ms
✓ readFileContent function > should successfully read the content of a text file
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 12:47:24
Duration 13ms
PASS Waiting for file changes...
press h to show help, press q to quit
This means your test ran successfully, and fs.readFile
was mocked as expected.
In some cases, you may need to reset mocks between tests to ensure they don’t interfere with each other. Vitest automatically resets mocks after each test, but you can manually reset them using mock.reset()
if you need finer control over the process.
For example, if you’re running multiple tests and want to ensure that each test starts with a clean mock state, you can add:
mock.reset(); // Clears all mocks
This ensures that mocks are cleared before each test, making your tests more isolated and predictable.
Now that you know how to mock external dependencies in Vitest, you'll explore setup and teardown hooks in the next section.
Step 7 — Using setup and teardown hooks
When writing multiple tests, you'll often need to perform the same setup or cleanup tasks before or after each test. Vitest provides a set of powerful hooks to help you manage these tasks more efficiently, keeping your test code clean and maintainable.
Let's build on our file reader example and introduce setup and teardown functionality using Vitest's built-in hooks. This will allow us to ensure mocks are properly reset and maintained across multiple tests.
Start by updating the fileReader.test.js
file to incorporate these setup/teardown hooks:
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { readFileContent } from "../fileReader.js";
import * as fs from "fs/promises";
// Use vi.mock to mock fs/promises
vi.mock("fs/promises", async () => {
return {
readFile: vi.fn()
};
});
describe("readFileContent function", () => {
beforeEach(() => {
// Runs before each test
vi.resetAllMocks();
// Set up default mock behavior for fs.readFile
fs.readFile.mockResolvedValue("Mocked content");
});
it("should successfully read the content of a text file", async () => {
const content = await readFileContent("text-content.txt");
// Check that fs.readFile was called once
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith("text-content.txt", "utf-8");
// Verify that the returned content matches the mock
expect(content).toBe("Mocked content");
});
});
The beforeEach
hook runs before each test. In this case, you use it to reset all mocks to their initial state and set a default mock implementation for fs.readFile
. This ensures that each test runs with a clean slate, avoiding any cross-test contamination.
Once you run this test, you should see output like the following:
✓ tests/fileReader.test.js (1 test) 2ms
✓ readFileContent function > should successfully read the content of a text file
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 13:45:07
Duration 14ms
PASS Waiting for file changes...
press h to show help, press q to quit
With this setup, you've successfully integrated setup hooks into your tests. Using beforeEach
, you can automate repetitive tasks such as resetting mocks or initializing resources. This leads to cleaner, more organized, and maintainable test code.
Beyond beforeEach
, Vitest offers additional hooks—afterEach
, beforeAll
, and afterAll
. These allow you to execute setup and teardown logic either before or after a group of tests, rather than for each individual test. This is especially useful when working with shared resources like database connections, ensuring efficient setup and cleanup.
For instance, when testing database operations, you can use beforeAll
to establish a connection once before any tests run and afterAll
to close the connection once all tests have completed:
describe("Database operations", () => {
beforeAll(async () => {
// Establish database connection
await db.connect();
});
afterAll(async () => {
// Close the database connection after all tests
await db.disconnect();
});
// Your tests here...
});
Using setup and teardown hooks ensures that tests remain isolated and run in a controlled environment. Automating setup tasks like mock resets and resource initialization reduces redundancy and makes the test suite more structured.
Now that you've incorporated setup and teardown hooks, the next step is to measure test coverage and ensure that all critical parts of your code are properly tested.
Step 8 — Code coverage with Vitest
Code coverage is a critical metric that helps you understand how much of your code is exercised by your tests. Vitest provides built-in coverage reporting capabilities, making identifying untested parts of your codebase easy and improving your testing strategy.
Vitest uses c8 for coverage reporting by default. To enable coverage reporting, you need to install the @vitest/coverage-v8
package:
npm i -D @vitest/coverage-v8
Once installed, you can run tests with coverage by using the --coverage
flag. Update the package.json
file to include a dedicated script for running tests with coverage:
{
. . .
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Now, you can run the coverage report with the following:
npm run test:coverage
This will run your tests and generate a detailed coverage report. The output will look something like this:
> vitest-demo@1.0.0 test:coverage
> vitest run --coverage
RUN v3.0.7
vitest-demo/vitest-demo
Coverage enabled with v8
✓ tests/math.test.js (2 tests) 2ms
✓ tests/fileReader.test.js (1 test) 2ms
Test Files 2 passed (2)
Tests 3 passed (3)
Start at 13:01:08
Duration 336ms (transform 31ms, setup 0ms, collect 35ms, tests 4ms, environment 0ms, prepare 120ms)
% Coverage report from v8
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 83.33 | 66.66 | 100 | 83.33 |
fileReader.js | 77.77 | 50 | 100 | 77.77 | 8-9
math.js | 100 | 100 | 100 | 100 |
The coverage report provides several key metrics, such as statement coverage, branch coverage, function coverage, and line coverage.
In the example above, you can see that math.js
has 100% coverage across all metrics, which means all code in this file is being tested.
However, fileReader.js
has only 77.77% line coverage and 50% branch coverage, indicating that the error handling path (lines 8-9) isn't being tested.
Modify the test for the error case in fileReader.js
to improve coverage:
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
...
describe("readFileContent function", () => {
beforeEach(() => {
....
});
it("should successfully read the content of a text file", async () => {
....
});
it("should throw an error when file reading fails", async () => {
// Set up mock to throw an error
fs.readFile.mockRejectedValue(new Error("File not found"));
// Expect the function to throw an error
await expect(readFileContent("non-existent.txt")).rejects.toThrow(
"Failed to read file: File not found"
);
expect(fs.readFile).toHaveBeenCalledTimes(1);
expect(fs.readFile).toHaveBeenCalledWith("non-existent.txt", "utf-8");
});
});
Running the coverage report again will show improved coverage:
✓ tests/math.test.js (2 tests) 2ms
✓ tests/fileReader.test.js (2 tests) 4ms
Test Files 2 passed (2)
Tests 4 passed (4)
Start at 13:50:41
Duration 353ms (transform 36ms, setup 0ms, collect 40ms, tests 5ms, environment 0ms, prepare 99ms)
% Coverage report from v8
---------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
fileReader.js | 100 | 100 | 100 | 100 |
math.js | 100 | 100 | 100 | 100 |
---------------|---------|----------|---------|---------|-------------------
Now, all files have 100% coverage, which means all code paths are being tested.
With coverage reports in place, you can adopt a coverage-driven testing approach. Run coverage reports regularly to identify untested code and focus on areas with low coverage first.
Step 9 — Exploring Vitest UI
Vitest offers a graphical user interface that provides a more interactive way to work with your tests. The UI offers valuable insights into your test suite, making debugging failing tests easier and analyzing execution times.
To enable the Vitest UI, install the package first:
npm i -D @vitest/ui
Then update your package.json
file to include a dedicated script:
{
. . .
"scripts": {
"test": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}
Now, you can launch the UI with the following command:
npm run test:ui
This command starts the Vitest UI server and automatically opens it in your default browser. If it doesn't open automatically, you can access it by navigating to http://localhost:51204/__vitest__/#/
in your browser.
The Vitest UI consists of three main sections:
- Test list panel – Displays all test files, their statuses, and execution times.
- Test details panel – Shows logs, errors, and assertions for individual test cases.
- Dashboard summary – Provides an overview of total tests, passed/failed counts, and overall test duration.
Tests can be rerun individually or as a group directly from the UI. Clicking the play button next to a test case re-executes only that test without running the entire suite.
Upon re-running a test, the module graph is updated, helping to visualize dependencies and track test execution flow.
For further analysis, the UI also allows viewing the test source code, making inspecting the logic behind failing tests easier.
The UI also provides interactive capabilities for running specific tests. You can:
- Run all tests with a single click
- Click on an individual test to run just that test
- Filter tests by status (passed, failed, skipped)
- View test execution times to identify slow tests
This visual approach to test execution simplifies debugging and speeds up development, especially when working with larger test suites.
Instead of manually modifying test files or running command-line filters, the UI enables quick navigation, targeted execution, and deeper insights into test performance.
Final thoughts
In this article, you've explored key features of Vitest. You've learned to set up a project, write tests, and leverage advanced features like filtering, mocking, and code coverage.
As you continue your testing journey, the official Vitest documentation is an excellent resource for exploring more advanced features, configuration options, and best practices
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github