Skip to Content
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes
Intermediate
0 likes
Courses
JavaScript Intermediate: Write Code That Doesn't Fall Apart When the Data Changes

Your First Tests With Vitest

Set up Vitest, write four tests for cart math, break one on purpose and read the failure. After this, you can check your money math in milliseconds instead of by clicking.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already have Vitest running with npm test, skip to "Reading a failing test".

Every time you change the discount code, you open the page, add two items, apply a code, and check the total by eye. That's a test. It's just slow, and you'll skip it on the day it matters. An automated test does the same check in milliseconds, every time, and it doesn't get bored. This lesson sets up Vitest, writes four tests, and breaks one on purpose so you can read the failure.

Why Vitest

There are several good test runners for JavaScript. Jest is the long-time standard and you'll find it in many existing projects. Vitest uses almost the same describe / it / expect style, works with import/export without extra setup, and is fast. For a new project, I'd pick Vitest. For an existing project that already uses Jest, keep Jest. Switching runners is not a good first task.

Setup

In an empty folder:

npm init -y
npm pkg set type=module
npm pkg set scripts.test="vitest run"
npm install --save-dev vitest

That installs Vitest as a dev dependency, makes .js files ES modules, and sets npm test to run all tests once. (Plain npx vitest without run stays open and reruns tests whenever you save, which is nice while you work.) The examples here were run with Vitest 5.0.4 on Node 24.

The code to test

cart.js:

export function subtotal(items) {
  return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}

export function applyDiscount(amount, percent) {
  if (percent < 0 || percent > 100) {
    throw new RangeError(`Discount must be 0 to 100, got ${percent}`);
  }
  return Math.round(amount * (100 - percent)) / 100;
}

The tests

Vitest finds files ending in .test.js (or .spec.js). Put this in cart.test.js, next to cart.js:

import { describe, it, expect } from "vitest";
import { subtotal, applyDiscount } from "./cart.js";

describe("subtotal", () => {
  it("adds price times quantity for every item", () => {
    const items = [
      { name: "Coffee", price: 180, qty: 2 },
      { name: "Bread", price: 65, qty: 1 },
    ];
    expect(subtotal(items)).toBe(425);
  });

  it("is 0 for an empty cart", () => {
    expect(subtotal([])).toBe(0);
  });
});

describe("applyDiscount", () => {
  it("takes 10% off", () => {
    expect(applyDiscount(425, 10)).toBe(382.5);
  });

  it("rejects a discount over 100%", () => {
    expect(() => applyDiscount(425, 150)).toThrow(RangeError);
  });
});
  • describe groups tests for one function.
  • it is one test. Write the name as a sentence about behaviour, so a failure reads like a bug report.
  • expect(actual).toBe(expected) is the check. Use toEqual for arrays and objects (it compares contents), toBe for numbers, strings and booleans.
  • toThrow needs a function that throws, so wrap the call: () => applyDiscount(425, 150). Without the wrapper, the error is thrown before expect even runs.

Run npm test:

 RUN  v5.0.4 /tmp/cart

 Test Files  1 passed (1)
      Tests  4 passed (4)

Reading a failing test

A test you've never seen fail might not be testing anything. So break the code on purpose. Change the discount line to Math.round(amount * percent) / 100 and run again:

 FAIL  cart.test.js > applyDiscount > takes 10% off
AssertionError: expected 42.5 to be 382.5 // Object.is equality

- Expected
+ Received

- 382.5
+ 42.5

 ❯ cart.test.js:20:36
     18| describe("applyDiscount", () => {
     19|   it("takes 10% off", () => {
     20|     expect(applyDiscount(425, 10)).toBe(382.5);
       |                                    ^

      Tests  1 failed | 3 passed (4)

Read it in order: which test (applyDiscount > takes 10% off), what it expected (382.5), what it got (42.5), and the line. 42.5 is 10% of 425, so the code applied the discount instead of the remainder. Put the line back, run again, all four pass.

⚠ Warning: Don't "fix" a failing test by changing the expected value to whatever the code returned. That turns the test into a recording of the bug. First decide which one is wrong, the code or the test. If you can't tell, ask whoever knows the business rule.

What to test first

You don't need 100% coverage. Start with:

  • Money math: totals, discounts, tax, rounding.
  • Rules that block bad input: negative quantity, invalid email.
  • Bugs you've already fixed once. Write a test that fails with the bug, then fix it, so it can't come back quietly.
  • Edge cases: empty list, zero, one item, the maximum.

💡 Tip: Keep tests fast and independent. A test shouldn't depend on another test running first, on today's date, or on a real API. If code needs a date, pass the date in as an argument so the test can choose it.

Common mistakes

  • Using toBe on objects or arrays (two arrays with the same items are not the same array).
  • Calling the function inside expect for toThrow without a wrapper.
  • Testing private details instead of results, so every refactor breaks the tests.
  • Writing tests that hit a real API and fail when the internet is slow.
  • Never seeing a test fail.

What happens if you skip this

Every change becomes a small gamble. You fix the discount for one promo and break rounding for another, and you find out when finance asks why the totals are off by a few centavos on a few hundred orders.

What's one calculation in your current project that would cost real money if it were wrong? Write its first test today.

About

Set up Vitest, write four tests for cart math, break one on purpose and read the failure. After this, you can check your money math in milliseconds instead of by clicking.

Duration
5 min
Last update
08/28/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.