Create your own
Lesson illustration

Vitest Setup and Code Coverage

Hello! Welcome to the final lesson in our "Project Setup & Tooling" module.

In our previous lessons, we've progressively built a robust foundation for our library. We initialized the project, configured our linter and formatter with BiomeJS, set up strict TypeScript rules, and fine-tuned our Vite build process to handle dual-module formats and external peer dependencies.

Today, we will complete our project setup by establishing a professional testing environment. A library without tests is a library that cannot be trusted. Our goal is to ensure that every piece of code we write from now on is verifiable and correct.

By the end of this lesson, you will be able to set up Vitest for unit testing and configure code coverage reporting. This will complete our tooling setup and prepare us to start developing the core features of our observable library with confidence.

1. Why Vitest?

Before we dive in, it's worth understanding why Vitest is an excellent choice for our project. As a seasoned developer, you've likely worked with various testing frameworks like Jest, Mocha, or Jasmine. Vitest was created to solve the specific challenges of testing in a modern, Vite-powered, ESM-first world.

To get a concise overview from the creator of Vitest himself, please watch the following segment.

Fast Unit Testing With Vitest

This clip from an interview with Anthony Fu, the creator of Vitest, explains the motivation behind its creation, highlighting the pain points with older tools like Jest in a Vite and ES Modules context.

Watch from 00:48 to 14:22. Focus on the discussion around why a new test runner was needed for Vite, the challenges with Jest's configuration duplication, and the shift towards native ES Modules (ESM).

The key takeaways are:

  • Unified Configuration: Vitest reads your vite.config.ts, so you don't need to maintain a separate, parallel configuration for your tests.
  • ESM & TypeScript First: It's designed from the ground up to work seamlessly with ES Modules and TypeScript, just like Vite itself.
  • Performance: It leverages Vite's on-demand architecture for a fast and responsive testing experience.

2. Installing and Configuring Vitest

Let's integrate Vitest into our project.

Step 1: Installation

First, add Vitest as a development dependency.

Getting Started | Guide

The official Vitest documentation provides the simplest installation instructions. We'll follow their 'Getting Started' guide.

Refer to the 'Adding Vitest to Your Project' section for the installation command. You only need to read that small part.

Run the following command in your terminal:

npm install -D vitest

Step 2: Add Test Scripts to package.json

Next, let's add scripts to package.json to run our tests. We'll add two scripts:

  1. test: Runs Vitest in "watch mode," which is ideal for development.
  2. coverage: Runs the tests once and generates a code coverage report.
// package.json
{
  // ... other properties
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "biome lint ./src",
    "format": "biome format --write ./src",
    "test": "vitest",
    "coverage": "vitest run --coverage"
  },
  // ... other properties
}

Notice the difference: vitest starts the watcher, while vitest run executes the test suite a single time, which is what you'd want for CI environments or when generating a report.

Step 3: Configure vite.config.ts

To enable full TypeScript support and autocompletion for Vitest's configuration options, we need to make a small adjustment to our vite.config.ts.

  1. Add a triple-slash directive at the top of the file.
  2. Import defineConfig from vitest/config instead of vite.
  3. Add the test property to the configuration object.

Your vite.config.ts should now look like this:

/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'
import pkg from './package.json'

export default defineConfig({
  plugins: [dts({ rollupTypes: true })],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyObservableFramework',
      fileName: (format) => `my-observable-framework.${format}.js`,
      formats: ['es', 'cjs'],
    },
    rollupOptions: {
      external: Object.keys(pkg.peerDependencies || {}),
    },
  },
  test: {
    // Vitest configuration will go here
  },
})

3. Writing Your First Test

By default, Vitest looks for files ending in .test.ts or .spec.ts. Let's create a simple test for a placeholder function.

First, create a new file src/utils.ts with a utility function we can test.

// src/utils.ts
export function isFunction(value: unknown): value is (...args: any[]) => any {
  return typeof value === 'function';
}

Now, create the corresponding test file src/utils.test.ts.

// src/utils.test.ts
import { describe, it, expect } from 'vitest';
import { isFunction } from './utils';

describe('isFunction', () => {
  it('should return true for functions', () => {
    expect(isFunction(() => {})).toBe(true);
    expect(isFunction(function() {})).toBe(true);
  });

  it('should return false for non-functions', () => {
    expect(isFunction(null)).toBe(false);
    expect(isFunction(undefined)).toBe(false);
    expect(isFunction('string')).toBe(false);
    expect(isFunction(123)).toBe(false);
    expect(isFunction({})).toBe(false);
    expect(isFunction([])).toBe(false);
  });
});

A few key points:

  • Unlike some older frameworks, Vitest requires you to explicitly import test utilities like describe, it (an alias for test), and expect. This improves clarity and avoids global namespace pollution.
  • The API is heavily inspired by Jest, so the describe/it/expect pattern should feel very familiar.

Now, run the test watcher:

npm run test

You should see output indicating that your tests have passed. Try making a change to utils.ts or utils.test.ts and watch how Vitest automatically re-runs the tests.

4. Configuring Code Coverage

Knowing that tests pass is good, but knowing what code your tests actually cover is even better. Code coverage is a critical metric for a library, as it helps identify untested parts of your codebase.

Step 1: Run the Coverage Script

Let's use the script we created earlier.

npm run coverage

The first time you run this, Vitest will prompt you to install a dependency (@vitest/coverage-v8). Type y and press Enter.

A terminal showing Vitest prompting the user to install the @vitest/coverage-v8 package to enable coverage reporting.

After the installation, Vitest will run the tests and display a coverage summary in your terminal.

Step 2: Fine-Tuning Coverage Configuration

The command-line flag is convenient, but for a permanent and more detailed setup, we'll use our vite.config.ts. We will configure three key aspects: the provider, the reporters, and which files to include.

Why Vitest Is Better Than Jest

This video provides a clear, concise walkthrough of setting up coverage, including generating an HTML report.

Watch from 05:47 to 08:17. This section covers adding the --coverage flag (which we've done), installing the coverage dependency, and then configuring the test.coverage object in vite.config.js to specify reporters like 'html'.

Based on the video and best practices, let's update the test object in vite.config.ts.

// vite.config.ts
// ...
  test: {
    coverage: {
      // The provider to use for coverage collection. v8 is the default and is fast.
      provider: 'v8',
      
      // A list of reporters to use.
      // 'text' shows a summary in the console.
      // 'html' generates a detailed report you can view in your browser.
      // 'json' is useful for programmatic consumption.
      reporter: ['text', 'html', 'json'],
      
      // By default, coverage is only collected for files that are imported in tests.
      // 'all: true' would include all files, but 'include' gives more control.
      // We want to ensure all source files are included in the report, even if they have 0% coverage.
      include: ['src/**/*.ts'],
      
      // We can exclude files from coverage, such as our main export file.
      exclude: ['src/index.ts'],
    },
  },
// ...

A quick explanation of these choices:

  • provider: 'v8': Vitest supports two coverage providers: v8 and istanbul. The v8 provider is native to Node.js and generally faster because it doesn't require instrumenting (rewriting) your source code before running tests. It's the recommended default.
  • reporter: ['text', 'html', 'json']: This tells Vitest to generate three types of reports. The HTML report is particularly useful for exploring coverage visually.
  • include: ['src/**/*.ts']: This is a crucial setting. Without it, a file with zero tests wouldn't even appear in the coverage report, potentially giving you a false sense of security. This ensures all your source files are accounted for.
  • exclude: ['src/index.ts']: We exclude index.ts because it will likely only contain export statements, which don't have executable logic to test.

Step 3: Viewing the HTML Report

Now, run the coverage script again:

npm run coverage

This time, you'll find a new coverage/ directory in your project root. Open the coverage/index.html file in your browser. You'll see a detailed, interactive report showing line-by-line coverage for your files. This is an indispensable tool for maintaining a high-quality library.

Conclusion

Congratulations! You have successfully set up a complete, professional-grade testing and coverage reporting pipeline for your project. This concludes our module on project setup and tooling. We are now fully equipped to start building our library's functionality with the assurance that our code is correct and maintainable.

Key Takeaways:

  • Vitest is a modern testing framework that integrates seamlessly with a Vite-based project, reusing its configuration and natively supporting ESM and TypeScript.
  • The test script (vitest) is used for development with watch mode, while vitest run is for single-run executions suitable for CI or reporting.
  • Code coverage is enabled via the --coverage flag or, for more control, through the test.coverage object in vite.config.ts.
  • Configuring coverage.include is essential to get an accurate picture of your project's total test coverage, including completely untested files.
  • The HTML coverage report provides a detailed, visual way to inspect which parts of your code are exercised by your tests.

Next Lesson Preview:
With our robust development environment in place, we will shift our focus to the library's architecture. In our next lesson, we will begin implementing the core of our reactive framework by designing and building the generic Observer interface and the Subscription class, the fundamental components of the observable pattern.

Can't find a good explanation? Sign up and we'll make it for you

Sign up