# A Beginner's Guide to Unit Testing with Vitest

[Vitest](https://vitest.dev/) 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](https://nodejs.org/en/download) 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:

```command
mkdir vitest-demo && cd vitest-demo
```

Then, initialize the directory as an npm project:

```command
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:

```command
npm pkg set type="module"
```

Now, install Vitest as a development dependency:

```command
npm install -D vitest
```

After installation, add a test script to your `package.json` file:

```json
[label package.json]
{
  . . .
  "scripts": {
[highlight]
    "test": "vitest"
[/highlight]
  }
}
```

You're now ready to create a simple ES Module function and test it using Vitest. Here’s the function in full:

```javascript
[label math.js]
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:

```command
mkdir tests
```

Next, create a file named `math.test.js` inside the `tests` directory and add the following test:

```javascript
[label tests/math.test.js]
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:

```command
npm test
```

Since the test script in `package.json` is set to `vitest`, this command is equivalent to:

```command
npx vitest
```

Vitest will automatically find and run test files inside the `tests` directory. The output should look something like this:

```text
[output]
 ✓ 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:

```javascript
[label tests/math.test.js]
describe("add function", () => {
[highlight]
  it.only("should return 3 when adding 1 and 2", () => {
[/highlight]
    expect(add(1, 2)).toBe(3);
  });
[highlight]
  it("should return 5 when adding 2 and 3", () => {
    expect(add(2, 3)).toBe(5);
  });
[/highlight]
});
```

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:


```text
[output]
 ✓ 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:

```javascript
[label tests/math.test.js]
...
describe("add function", () => {
[highlight]
  it.skip("should return 3 when adding 1 and 2", () => {
[/highlight]
    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:

```text
[output]
  ✓ 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:

```javascript
[label tests/math.test.js]
...
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:

```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:

```text
[output]
  ✓ 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:

```command
npm test -- -t "should return 3"
```

```text
[output]
 ✓ 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:

```javascript
[label math.js]
[highlight]
import { it, expect } from 'vitest';
[/highlight]

export function add(a, b) {
  return a + b;
}

[highlight]
// 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);
  });
}
[/highlight]
```

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:

```text
[output]
 ✓ 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:

```javascript
[label math.js]
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`:

```command
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:

```javascript
[label fileReader.js]
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.

```javascript
[label tests/fileReader.test.js]
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:

```text
[output]
 ✓ 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:

```javascript
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:

```javascript
[label tests/fileReader.test.js]
[highlight]
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
[/highlight]
import { readFileContent } from "../fileReader.js";
import * as fs from "fs/promises";

// Use vi.mock to mock fs/promises
vi.mock("fs/promises", async () => {
  return {
[highlight]
    readFile: vi.fn()
[/highlight]
  };
});

describe("readFileContent function", () => {
[highlight]
 beforeEach(() => {
    // Runs before each test
    vi.resetAllMocks();
    
    // Set up default mock behavior for fs.readFile
    fs.readFile.mockResolvedValue("Mocked content");
  });
[/highlight]

  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:

```text
[output]
 ✓ 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:

```javascript
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](https://github.com/bcoe/c8) for coverage reporting by default. To enable coverage reporting, you need to install the `@vitest/coverage-v8` package:

```command
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:

```json
[label package.json]
{
  . . .
  "scripts": {
    "test": "vitest",
[highlight]
    "test:coverage": "vitest run --coverage"
[/highlight]
  }
}
```

Now, you can run the coverage report with the following:

```command
npm run test:coverage
```

This will run your tests and generate a detailed coverage report. The output will look something like this:

```text
[output]
> 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:

```javascript
[label tests/fileReader.test.js]
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
...
describe("readFileContent function", () => {
  beforeEach(() => {
    ....
  });

  
  it("should successfully read the content of a text file", async () => {
    ....
  });
[highlight]
  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");
  });
[/highlight]
});
```

Running the coverage report again will show improved coverage:

```text
[output]

 ✓ 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. 

[ad-logs]

## 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:

```command
npm i -D @vitest/ui
```

Then update your `package.json` file to include a dedicated script:

```json
[label package.json]
{
  . . .
  "scripts": {
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
[highlight]
    "test:ui": "vitest --ui"
[/highlight]
  }
}
```

Now, you can launch the UI with the following command:

```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.

![Screenshot of Vitest UI](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/7a5904d5-70fb-42bf-1958-a33f9c6bf500/md1x =3248x1994)


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.

![Screenshot of the play button](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/8720bf25-5241-4027-3363-b4d12a55a800/md2x =3034x1500)

Upon re-running a test, the module graph is updated, helping to visualize dependencies and track test execution flow.

![Screenshot of the module graph](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/bbbc3108-b335-43aa-f8a6-80f03b9e0900/md1x =3248x1994)

For further analysis, the UI also allows viewing the test source code, making inspecting the logic behind failing tests easier.


![source-code.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ed66cfb3-78b4-4c8d-f448-2f9c62f34200/lg2x =3248x1994)

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](https://vitest.dev/advanced/api/) is an excellent resource for exploring more advanced features, configuration options, and best practices
