DEV Community

Alex Spinov
Alex Spinov

Posted on

Vitest Has a Free API — The Fastest Unit Testing Framework

Vitest is the blazing fast unit test framework powered by Vite — it's Jest-compatible, runs ESM natively, and is 2-10x faster. Completely free.

Why Vitest Over Jest?

  • Vite-powered — uses Vite's transform pipeline, instant HMR for tests
  • Jest-compatible — same API (describe, it, expect), easy migration
  • ESM native — no more babel-jest or ts-jest configs
  • In-source testing — write tests next to your code
  • Browser mode — run tests in real browsers
  • UI dashboard — visual test explorer

Quick Start

npm install -D vitest
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Writing Tests

// math.test.ts
import { describe, it, expect } from "vitest";
import { add, multiply, divide } from "./math";

describe("Math utilities", () => {
  it("adds two numbers", () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  it("multiplies two numbers", () => {
    expect(multiply(3, 4)).toBe(12);
  });

  it("throws on division by zero", () => {
    expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
  });
});
Enter fullscreen mode Exit fullscreen mode

Async Testing

import { describe, it, expect, vi } from "vitest";

describe("API client", () => {
  it("fetches users", async () => {
    const users = await fetchUsers();
    expect(users).toHaveLength(2);
    expect(users[0]).toHaveProperty("name");
  });

  it("handles API errors", async () => {
    vi.spyOn(global, "fetch").mockRejectedValueOnce(new Error("Network error"));

    await expect(fetchUsers()).rejects.toThrow("Network error");
  });
});
Enter fullscreen mode Exit fullscreen mode

Mocking

import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendEmail } from "./email";
import * as mailer from "./mailer";

// Mock entire module
vi.mock("./mailer");

describe("Email service", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("sends welcome email", async () => {
    vi.mocked(mailer.send).mockResolvedValue({ id: "msg-1" });

    const result = await sendEmail("user@test.com", "Welcome!");

    expect(mailer.send).toHaveBeenCalledWith({
      to: "user@test.com",
      subject: "Welcome!",
    });
    expect(result.id).toBe("msg-1");
  });

  it("retries on failure", async () => {
    vi.mocked(mailer.send)
      .mockRejectedValueOnce(new Error("timeout"))
      .mockResolvedValueOnce({ id: "msg-2" });

    const result = await sendEmail("user@test.com", "Hello");

    expect(mailer.send).toHaveBeenCalledTimes(2);
    expect(result.id).toBe("msg-2");
  });
});
Enter fullscreen mode Exit fullscreen mode

Snapshot Testing

import { it, expect } from "vitest";

it("renders user card", () => {
  const html = renderUserCard({ name: "Alice", role: "Admin" });
  expect(html).toMatchInlineSnapshot(`
    "<div class=\\"card\\">
      <h2>Alice</h2>
      <span class=\\"badge\\">Admin</span>
    </div>"
  `);
});

it("serializes config correctly", () => {
  const config = generateConfig({ env: "production" });
  expect(config).toMatchSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

In-Source Testing (Unique to Vitest)

// utils.ts
export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/\s+/g, "-")
    .replace(/[^a-z0-9-]/g, "");
}

// Tests live IN the source file!
if (import.meta.vitest) {
  const { describe, it, expect } = import.meta.vitest;

  describe("slugify", () => {
    it("converts spaces to hyphens", () => {
      expect(slugify("hello world")).toBe("hello-world");
    });

    it("removes special characters", () => {
      expect(slugify("Hello, World!")).toBe("hello-world");
    });

    it("handles multiple spaces", () => {
      expect(slugify("foo  bar  baz")).toBe("foo-bar-baz");
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Tests are tree-shaken out of production builds.

Testing React Components

// Button.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";

describe("Button", () => {
  it("renders text", () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText("Click me")).toBeDefined();
  });

  it("calls onClick", () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Click</Button>);

    fireEvent.click(screen.getByText("Click"));
    expect(onClick).toHaveBeenCalledOnce();
  });
});
Enter fullscreen mode Exit fullscreen mode

Coverage

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    coverage: {
      provider: "v8", // or 'istanbul'
      reporter: ["text", "json", "html"],
      exclude: ["node_modules/", "test/"],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Vitest vs Jest vs Mocha vs Node Test Runner

Feature Vitest Jest Mocha Node --test
Speed Fastest Fast Medium Fast
ESM support Native Experimental Plugin Native
TypeScript Native (Vite) ts-jest ts-node tsx
Watch mode Instant (HMR) Good Plugin Basic
In-source tests Yes No No No
Browser mode Yes jsdom jsdom No
UI Built-in No No No
Coverage Built-in Built-in Plugin Built-in
Config vite.config jest.config .mocharc None

Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.

Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.

Top comments (0)