# Jest and Vitest

> JavaScript and TypeScript testing: Jest's batteries-included matchers, mocks, and snapshots - and Vitest, the faster, Vite-native drop-in with the same API.


---

# Jest and Vitest

You wrote some JavaScript, it works on your machine, and now someone wants tests. You open the docs and drown in `describe`, `it`, `beforeEach`, `jest.fn()`, `toHaveBeenCalledWith`, snapshots, and two tools that look nearly identical. This guide cuts through it. You will learn one mental model that covers both Jest and Vitest, write tests you trust, and know exactly when to reach for which.

## How to read this

Read it in order the first time. Phase 1 gives you the shape of a test and why these tools exist. Phase 2 is the daily work: matchers, mocks, async, timers - the stuff you'll use every hour. Phase 3 is where tests go wrong: snapshot rot, flaky timers, mock leakage, and choosing between the two runners. If you've never written a unit test, read [Your First Unit Test](/guides/your-first-unit-test) first; this guide assumes you know what an assertion is.

## The phases

1. [The model: what a test runner does](01-the-test-runner-model.md)
2. [The daily core: matchers, mocks, async, timers](02-matchers-mocks-async.md)
3. [Production reality: snapshots, flakiness, and Jest vs Vitest](03-snapshots-flakiness-choosing.md)


---

# The model: what a test runner does

Here's the reality most tutorials skip. A test is not magic. It's a function that runs your code and then checks whether the result is what you expected. If the check fails, the function throws. That's the whole idea. Everything else - `describe`, `it`, mocks, snapshots - is sugar on top of "run code, throw if wrong."

A **test runner** is the program that finds those functions, runs them, catches the throws, and prints a report. Jest and Vitest are both test runners. They give you four things you'd otherwise build yourself: a way to *find* test files, a way to *group and name* tests, a library of *assertions* (matchers), and tooling for the hard parts (mocks, timers, coverage). Once you see them as "the same four jobs," the two tools stop being two things to learn.

## Why these tools exist at all

You could test without a runner. Write a script, call your function, and `throw new Error()` if the answer is wrong:

```js
import { add } from "./math.js";

if (add(2, 3) !== 5) {
  throw new Error(`add(2,3) was ${add(2, 3)}, expected 5`);
}
console.log("ok");
```

*What just happened:* this is a real test. It runs `add`, checks the result, and crashes loudly if it's wrong. But it stops at the first failure, gives no nice output, and you'd hand-roll grouping and mocks. A runner does all of that for you and keeps going after a failure so you see every problem at once.

That's the trade. You adopt a runner not because your code can't be tested without one, but because the runner turns "a pile of throwing scripts" into a report you can actually read, run in CI, and trust.

## The shape of every test

Both tools use the same vocabulary, borrowed from a style called BDD. You'll see it everywhere:

```js
import { describe, it, expect } from "vitest"; // or "@jest/globals"
import { add } from "./math.js";

describe("add", () => {
  it("sums two numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  it("handles negatives", () => {
    expect(add(-1, -1)).toBe(-2);
  });
});
```

*What just happened:* `describe` groups related tests under a label. `it` (its alias `test` is identical) declares one test with a sentence describing the behavior. `expect(...)` wraps the value you got, and `.toBe(...)` is the matcher that throws if it doesn't match. Read the `it` line as a sentence: "add sums two numbers." If that sentence is true after the test runs, it passes.

The naming matters more than it looks. A good `it` description tells a future reader *what the code is supposed to do*, so a failing test reads like a bug report: "add sums two numbers - FAILED." Write the sentence first, then make it true.

> **Jest vs Vitest, line one.** The only difference in the block above is the import: `vitest` vs `@jest/globals`. Jest also injects `describe`/`it`/`expect` as globals by default, so you often see no import at all. Vitest can do the same with `globals: true` in its config. The test bodies are otherwise identical - that sameness is the whole reason this guide covers both at once.

## Where the runner looks

A runner needs to know which files are tests. Both default to filename patterns: anything matching `*.test.js`, `*.spec.js` (and their `.ts`/`.jsx`/`.tsx` cousins), or files inside a `__tests__` folder.

```text
src/
  math.js
  math.test.js        ← found: matches *.test.js
  utils/
    format.ts
    format.spec.ts     ← found: matches *.spec.ts
  __tests__/
    routing.test.ts    ← found: inside __tests__
```

*What just happened:* you didn't register these files anywhere. You named them by convention and the runner discovered them. Co-locating `math.test.js` next to `math.js` is the common style - the test sits next to the thing it tests, so it's quick to find and stays in sync.

## Running it

You invoke the runner from an npm script. Both tools watch for changes during development and run once in CI.

```bash
# package.json scripts: { "test": "vitest" }  or  { "test": "jest" }
npm test              # Vitest watches by default; Jest runs once
npx vitest run        # run once and exit (what CI uses)
npx vitest            # interactive watch mode
npx jest --watch      # Jest's watch mode (opt-in)
```

*What just happened:* one command finds every test file, runs every `it`, and prints pass/fail counts plus the file and line of any failure. In watch mode the runner re-runs only the tests affected by the file you saved, which is why a tight test loop feels instant. Note the defaults differ: Vitest watches unless you say `run`; Jest runs once unless you say `--watch`.

For builders: keep `"test": "vitest"` (or `jest`) as your dev script and add `"test:ci": "vitest run --coverage"` for the pipeline. Same tool, two intentions - fast feedback locally, a clean one-shot run with coverage in CI. For where unit tests sit relative to integration and end-to-end tests, see [Unit, Integration, and E2E](/guides/unit-integration-e2e).

```quiz
[
  {
    "q": "At its core, what makes a test 'fail'?",
    "choices": ["The runner returns false", "An assertion throws an error", "console.log prints 'fail'", "The file is renamed"],
    "answer": 1,
    "explain": "A matcher like toBe throws when the value doesn't match; the runner catches that throw and marks the test failed."
  },
  {
    "q": "How does Jest or Vitest know which files are tests?",
    "choices": ["You list them in a manifest", "By filename patterns like *.test.js and __tests__ folders", "Any file that imports expect", "It runs every file in src/"],
    "answer": 1,
    "explain": "Both discover tests by convention: *.test/*.spec filenames and __tests__ directories, no manual registration."
  },
  {
    "q": "What is the relationship between describe and it?",
    "choices": ["describe runs code, it asserts", "describe groups and labels related tests, it declares one test", "it must come before describe", "They are interchangeable aliases"],
    "answer": 1,
    "explain": "describe is a labeled group of tests; each it (or test) is a single named behavior with its own assertions."
  }
]
```


---

# The daily core: matchers, mocks, async, timers

This is the phase you'll live in. Once tests are running, ninety percent of your time is spent on four skills: picking the right matcher, faking a dependency, testing code that's asynchronous, and controlling time. Get fluent here and you can test almost anything. The good news from Phase 1 holds: the API is nearly identical in Jest and Vitest, so what you learn here works in both.

## Matchers: say what you mean

A matcher is the assertion. The skill is choosing the one that says exactly what you mean, because the matcher decides how good the failure message is.

```js
expect(add(2, 3)).toBe(5);              // strict ===, for primitives
expect({ a: 1 }).toEqual({ a: 1 });     // deep equality, for objects/arrays
expect([1, 2, 3]).toContain(2);          // membership
expect(user.name).toBeDefined();         // not undefined
expect(isReady).toBe(true);              // exact boolean
expect(() => parse("")).toThrow();       // the function throws
expect("hello world").toMatch(/world/);  // string matches regex
```

*What just happened:* the big trap is `toBe` versus `toEqual`. `toBe` uses `===`, which for objects means "the same reference in memory." Two objects with identical contents are *not* `===`, so `expect({a:1}).toBe({a:1})` fails. Use `toEqual` to compare contents. Reach for `toBe` only with primitives (numbers, strings, booleans) or when you genuinely mean "the exact same object."

When a matcher fails, the runner prints a diff of expected vs received. That diff is your debugging tool, so picking the precise matcher (`toContain` instead of asserting on `arr.indexOf(x) !== -1`) is what makes failures readable instead of cryptic.

## Mocks: replacing the parts you don't want to run

Your function calls a database, an HTTP API, or a clock. You don't want the test to actually hit the network - that's slow, flaky, and not what you're testing. A **mock** is a stand-in: a fake function you control, that records how it was called.

```js
import { vi } from "vitest"; // Jest: use `jest` instead of `vi`, same methods

const onSave = vi.fn();          // a fake function
onSave("draft");
onSave("final");

expect(onSave).toHaveBeenCalledTimes(2);
expect(onSave).toHaveBeenCalledWith("final");   // checks any call
expect(onSave).toHaveBeenLastCalledWith("final"); // checks the last one
```

*What just happened:* `vi.fn()` (Jest: `jest.fn()`) creates a function that does nothing but remember every call. You pass it where a real callback would go, run your code, then assert on *how it was called*. This is how you test "did my code notify the listener with the right argument?" without caring what the listener does.

You can also make a mock return a value or resolve a promise:

```js
const fetchUser = vi.fn();
fetchUser.mockResolvedValue({ id: 1, name: "Ada" });

const user = await fetchUser(1);
expect(user.name).toBe("Ada");
expect(fetchUser).toHaveBeenCalledWith(1);
```

*What just happened:* `mockResolvedValue` makes the fake return a resolved promise, so `await` gets your canned object. Now you can test code that depends on `fetchUser` without a real server. To replace a whole imported module, use `vi.mock("./api.js", ...)` (Jest: `jest.mock`) - same idea, applied to every export of a module.

> **Mock the boundary, not the logic.** Mock the things you don't own or don't want to run: the network, the filesystem, the clock, third-party SDKs. Don't mock the function you're actually testing - if you mock everything, the test passes even when the real code is broken. A test full of mocks is often a test that proves nothing.

## Async: await the result, or the test lies

The single most common testing bug: forgetting that the code is asynchronous. If you don't `await`, the test function returns before the assertion runs, and the runner calls it green.

```js
it("loads the user", async () => {
  const user = await loadUser(1);     // MUST await
  expect(user.name).toBe("Ada");
});

it("rejects on missing user", async () => {
  await expect(loadUser(999)).rejects.toThrow("not found");
});
```

*What just happened:* mark the test `async` and `await` the promise so the assertion runs before the test ends. For promises that should *fail*, use `await expect(promise).rejects.toThrow(...)` - and keep the `await`, or the rejection escapes and the test passes wrongly. The matching `.resolves` checks a fulfilled promise's value. The rule is simple: if there's a promise anywhere in the test, there must be an `await`.

## Timers: stop waiting for real time

Code that uses `setTimeout`, `setInterval`, or debounce should not make your test sleep for real seconds. **Fake timers** let you fast-forward the clock instantly.

```js
import { vi, it, expect } from "vitest"; // Jest: jest.useFakeTimers(), etc.

it("calls back after the delay", () => {
  vi.useFakeTimers();
  const cb = vi.fn();

  setTimeout(cb, 1000);
  expect(cb).not.toHaveBeenCalled();   // time hasn't moved

  vi.advanceTimersByTime(1000);         // jump forward 1s, instantly
  expect(cb).toHaveBeenCalledOnce();

  vi.useRealTimers();                   // restore for other tests
});
```

*What just happened:* `useFakeTimers()` swaps the real `setTimeout` for a controllable fake. The callback hasn't fired yet because no time has "passed." `advanceTimersByTime(1000)` simulates one second passing and runs anything scheduled in that window - with no actual waiting. Always call `useRealTimers()` afterward (or in an `afterEach`) so fake time doesn't leak into the next test. The Jest API is the same with `jest.` in front: `jest.useFakeTimers()`, `jest.advanceTimersByTime(...)`.

For builders: combine fake timers with a mock callback to test debouncing - fire the function five times, advance the clock, assert the callback ran *once*. That's a test that would take real seconds and be flaky if you used real timers.

```quiz
[
  {
    "q": "You compare two objects with identical contents using toBe. What happens?",
    "choices": ["Passes - contents match", "Fails - toBe uses === (reference equality)", "Throws a syntax error", "Passes only for empty objects"],
    "answer": 1,
    "explain": "toBe is reference equality (===); two distinct objects are never ===. Use toEqual for deep content comparison."
  },
  {
    "q": "Your async test forgets to await the promise. What's the likely result?",
    "choices": ["The test errors immediately", "The test passes even when the assertion would fail", "The runner retries it", "The promise is awaited automatically"],
    "answer": 1,
    "explain": "Without await, the test function returns before the assertion runs, so the runner reports a false green."
  },
  {
    "q": "Why use fake timers instead of letting setTimeout run for real?",
    "choices": ["Real timers are not supported in tests", "To advance time instantly and avoid slow, flaky waits", "Fake timers improve code coverage", "They mock the network too"],
    "answer": 1,
    "explain": "Fake timers let you advanceTimersByTime to fast-forward instantly, so timer-based code tests fast and deterministically."
  }
]
```


---

# Production reality: snapshots, flakiness, and Jest vs Vitest

You can write tests. Now comes the part nobody warns you about: tests that lie, tests that pass on Tuesday and fail on Wednesday, and a snapshot folder no human has read in months. This phase is the gotchas - the failure modes that erode trust in a test suite - plus the decision you came here for: Jest or Vitest.

## Snapshots: the sharpest double-edged tool

A snapshot test serializes a value to a `.snap` file the first time it runs, then on every later run compares the new output against the stored file. It's seductive for component output and large objects because you write almost nothing.

```js
it("renders the invoice summary", () => {
  const html = renderInvoice({ total: 42, currency: "USD" });
  expect(html).toMatchSnapshot();   // first run: saves it. later: compares.
});
```

*What just happened:* the first run wrote the rendered HTML into a `__snapshots__/*.snap` file and passed. Every later run compares against that file and fails if the output changed. You asserted nothing specific - you asserted "the output is the same as last time." That's powerful and dangerous in equal measure.

Here's the abuse pattern. Output changes (often legitimately), the test fails, and a tired developer runs the update command without reading the diff:

```bash
npx vitest run -u        # -u / --updateSnapshot: overwrite all snapshots
npx jest -u              # same flag in Jest
```

*What just happened:* every failing snapshot was overwritten with the current output, failures and all. If a bug changed the output, you've enshrined the bug as the new "correct" answer. This is how snapshot suites rot into meaningless green.

> **Snapshot rules that keep them trustworthy.** Keep snapshots small and reviewable - a giant blob nobody reads catches nothing. Treat a snapshot diff in code review like any other code change; read it. Prefer an explicit matcher (`toBe`, `toEqual`, `toContain`) whenever you can name what you expect - `expect(total).toBe(42)` tells a reader the intent; a snapshot doesn't. Use snapshots for output that's tedious to assert by hand, not as a substitute for thinking.

## Flakiness: the trust killer

A flaky test passes and fails without the code changing. One flaky test trains your team to ignore red, and an ignored suite is worthless. The usual causes are all about hidden state and timing:

```text
Common flake sources and the fix:
  real timers / sleeps        → fake timers (Phase 2)
  shared state between tests   → reset in beforeEach / afterEach
  test order dependence        → tests must pass in any order, in isolation
  unmocked network or clock    → mock the boundary
  un-awaited promises          → await everything async
```

*What just happened:* every line is a leak of state or time across the boundary of a single test. The cure is isolation - each test sets up what it needs and cleans up after itself, so order and timing can't matter. Both runners give you the hook to enforce it:

```js
import { afterEach, vi } from "vitest"; // Jest: import from @jest/globals

afterEach(() => {
  vi.restoreAllMocks();   // undo spies/mocks
  vi.useRealTimers();     // undo fake timers
});
```

*What just happened:* after every test, mocks and fake timers are reset so the next test starts clean. Without this, a mock set in one test silently changes behavior in the next, and you get a failure that only appears when tests run in a certain order - the worst kind to debug. Both tools also have a config flag (`restoreMocks`/`clearMocks`) that does this automatically; turning it on is a cheap insurance policy.

## Coverage: a flashlight, not a scoreboard

Both runners produce coverage reports - which lines ran during the tests.

```bash
npx vitest run --coverage
npx jest --coverage
# prints a table: % Stmts | % Branch | % Funcs | % Lines  per file
```

*What just happened:* the report shows which code your tests exercised. Use it as a flashlight to find *untested* areas - a file at 10% is telling you something. Do not turn it into a target: 100% coverage proves every line *ran*, not that any behavior is *correct*. A test with no assertions can hit 100% and verify nothing. Chase meaningful tests; let coverage point you at the gaps.

## Jest vs Vitest: the actual decision

You've seen across every phase that the APIs are nearly the same. So the choice is about your build, not your test syntax.

```text
Pick Vitest when:
  - your app already uses Vite (React/Vue/Svelte via Vite, SvelteKit, etc.)
  - you write native ESM and TypeScript and want it to work with no fuss
  - test startup/watch speed matters (it shares Vite's transform pipeline)

Pick Jest when:
  - you're on a build that isn't Vite (Create React App legacy, Next.js
    defaults, plain Node, React Native / Metro)
  - you inherit a large existing Jest suite - no reason to migrate
  - you need a specific Jest-ecosystem plugin without a Vitest equivalent
```

*What just happened:* the rule of thumb is "match your bundler." Vitest reuses your Vite config and transforms, so a Vite project gets fast, ESM-native, TypeScript-aware testing with almost no setup. Jest is the mature default everywhere else, with the largest ecosystem and the most Stack Overflow answers. Vitest was deliberately built API-compatible with Jest, which is why migrating is often mostly a find-and-replace of `jest.` to `vi.` - and why learning one taught you both.

In the wild: a team on plain Node or an older React stack stays on Jest because it works and the suite is large; a new Vite or SvelteKit app reaches for Vitest because it inherits the existing build config and the watch loop is noticeably snappier. Neither is "better" in the abstract - the right one is the one that matches the toolchain you already have. For how these unit tests fit a broader strategy, revisit [Unit, Integration, and E2E](/guides/unit-integration-e2e).

```quiz
[
  {
    "q": "Why is blindly running the snapshot update flag (-u) dangerous?",
    "choices": ["It deletes test files", "It overwrites snapshots with current output, enshrining any bug as correct", "It disables coverage", "It only works in Jest"],
    "answer": 1,
    "explain": "-u overwrites stored snapshots with whatever the code produces now; an un-reviewed bug becomes the new baseline."
  },
  {
    "q": "Which is the strongest single defense against test order-dependence and flakiness?",
    "choices": ["Higher coverage", "Resetting mocks and timers in afterEach so each test is isolated", "More snapshots", "Running tests slower"],
    "answer": 1,
    "explain": "Cleaning up mocks/timers after each test prevents state leaking across tests, which is what makes order matter."
  },
  {
    "q": "Your app is built with Vite. Which runner fits best, and why?",
    "choices": ["Jest, because it's older", "Vitest, because it reuses your Vite config and transform pipeline", "Either is identical in setup", "Jest, because Vitest can't do TypeScript"],
    "answer": 1,
    "explain": "Vitest shares Vite's config and transforms, giving fast, ESM- and TS-native testing with minimal setup on a Vite project."
  }
]
```
