# Your First Tests with Vitest

A unit test is a few lines of code that check one piece of your code and shout when it breaks, and Vitest's describe, it and expect let you write one, run it and read the result.

Journeyman · JavaScript · Unit Testing · Updated November 1, 2026

## After this lesson you can

- Write a unit test with Vitest's describe, it and expect, and read its result
- Choose between toBe and toEqual for what a test compares
- Write a failing test first, then the code that makes it pass

## Key takeaways

- A unit test calls one piece of your code with a known input and checks the answer. Like a smoke alarm, it stays quiet until something is wrong.
- In Vitest, describe groups tests, it names one behavior, and expect(actual).toBe(expected) checks one answer.
- toBe compares single values such as numbers and text. toEqual compares what's inside an object or an array.
- A failing test shows what it expected and what it got. Read both before you change any code.
- Write the test first and watch it fail, then write the code that makes it pass. npm test keeps watching and runs the tests again on every save.

Every exercise lesson in this course came with tests. You wrote a function, ran the tests, and watched red turn into green, and each red line told you what was still wrong. Somebody wrote those tests. From this lesson on, that somebody is you. Tests are what a code review asks for first ("where are the tests?"), what lets a team change a price rule on a Friday without breaking the checkout, and what makes a project on your GitHub look professional.

This chapter's running example is the Starlight Cinema, a movie theater whose ticket booth needs prices you can trust: a matinee price, an evening price, and discounts for students and seniors.

## A smoke alarm for your code

A smoke alarm does nothing all day. It doesn't cook dinner or make the house nicer. It sits on the ceiling, quiet, and the moment there's smoke, it shouts. You hang one so you don't have to check the kitchen every five minutes.

A **test** is a smoke alarm for your code: a few lines that run a piece of your program with a known input and check the answer. A **unit test** checks one small piece, a *unit*, usually one function. The check itself, "this should be 9", is called an **assertion**. You can write one with what you already know:

```javascript
function showPrice(hour) {
  if (hour < 17) {
    return 9;
  }
  return 14;
}

const price = showPrice(14);
if (price === 9) {
  console.log('PASS: a 2 PM show costs $9');
} else {
  console.log(`FAIL: a 2 PM show should cost $9, but showPrice gave $${price}`);
}
```

```text
PASS: a 2 PM show costs $9
```

`hour` uses the 24-hour clock, so 14 is 2 PM and 17 is 5 PM. Shows before 5 PM are matinees at $9; later shows cost $14. If someone changes `hour < 17` to `hour < 13`, the same file prints `FAIL: a 2 PM show should cost $9, but showPrice gave $14`, and the alarm goes off.

That works for one check. For hundreds, you want a **test runner**: a tool that finds every test, runs them all, and reports which passed. This course uses **Vitest** (read "vee-test"), a test runner made by the team behind Vite.

## Setting up Vitest

A project gets Vitest from npm, like any other package (the lesson *npm and Packages* goes into npm in depth). To follow along, make a folder named `starlight-cinema`, open it in VS Code, and create a `package.json` with this in it:

```json
{
  "name": "starlight-cinema",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest --reporter=tree"
  },
  "devDependencies": {
    "vitest": "5.0.5"
  }
}
```

- **`"type": "module"`** lets your files use `import` and `export`.
- **The `test` script** runs Vitest. `--reporter=tree` asks it to list every test by name, grouped the way you wrote them.
- **`devDependencies`** names Vitest, pinned to the exact version 5.0.5, so the lesson's output matches yours. It's a dev dependency because only you need it while you work; the people using your app never do.

In VS Code's terminal, in that folder, run `npm install` once. It downloads Vitest into a `node_modules` folder.

## describe, it and expect

The code to test goes in `tickets.js`. `export` lets other files, including tests, use the function:

```javascript
// Starlight Cinema's ticket prices.
export function showPrice(hour) {
  if (hour < 17) {
    return 9;
  }
  return 14;
}
```

Tests go in a file beside it whose name ends in `.test.js`: Vitest finds those by their name. Here's `tickets.test.js`:

```javascript
import { describe, it, expect } from 'vitest';
import { showPrice } from './tickets.js';

describe('showPrice', () => {
  it('charges the matinee price for a 2 PM show', () => {
    expect(showPrice(14)).toBe(9);
  });

  it('charges the evening price for a 7 PM show', () => {
    expect(showPrice(19)).toBe(14);
  });
});
```

Read it out loud and it almost makes a sentence: *describe showPrice: it charges the matinee price for a 2 PM show; expect showPrice of 14 to be 9.*

- **`describe(name, () => { ... })`** groups tests about one thing. The `=>` is read "arrow": each group and each test is an arrow function that Vitest calls when it runs them.
- **`it(name, () => { ... })`** is one test, named for one behavior. `test(...)` is another name for the same thing; this course uses `it`.
- **`expect(actual).toBe(expected)`** is the assertion. `actual` is what your code gave back; `expected` is what it should be. `toBe` is called a **matcher**: it decides whether the two match.

Run `npm test`. Vitest prints:

```text
 ✓ tickets.test.js (2 tests) 2ms
   ✓ showPrice (2)
     ✓ charges the matinee price for a 2 PM show 1ms
     ✓ charges the evening price for a 7 PM show 0ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
```

Each ✓ (a check mark) is a test that passed, and the times are how long each one took. Then Vitest keeps running: more on that below.

## Write the test first

Here's the habit that turns tests from a chore into a tool. Before you write a new feature, write the test for it, run it, and watch it fail. The ticket booth needs `ticketPrice(hour, guest)`: students pay $2 less, seniors $3 less. The function doesn't exist yet, but the tests can:

```javascript
describe('ticketPrice', () => {
  it('takes $2 off for a student', () => {
    expect(ticketPrice(19, 'student')).toBe(12);
  });

  it('takes $3 off for a senior', () => {
    expect(ticketPrice(19, 'senior')).toBe(11);
  });
});
```

Add `ticketPrice` to the file's import (`import { showPrice, ticketPrice } from './tickets.js';`), save, and both new tests fail, which is the point. Vitest marks them with × (a cross) and says why:

```text
 FAIL  tickets.test.js > ticketPrice > takes $2 off for a student
TypeError: ticketPrice is not a function
 ❯ tickets.test.js:16:12
```

The last line is the file, the line and the column where it happened. A first try at the function handles students only, and the senior test still fails, this time with the two values side by side:

```text
 FAIL  tickets.test.js > ticketPrice > takes $3 off for a senior
AssertionError: expected 14 to be 11 // Object.is equality

- Expected
+ Received

- 11
+ 14
```

**Expected** is what the test wanted (11); **Received** is what the code gave back (14): no discount at all. Read both before you change anything. Then finish the function in `tickets.js`:

```javascript
export function ticketPrice(hour, guest) {
  const price = showPrice(hour);
  if (guest === 'student') {
    return price - 2;
  }
  if (guest === 'senior') {
    return price - 3;
  }
  return price;
}
```

Save, and all four tests pass. That loop has a name: **red, green, refactor**. Red: a failing test that says what you want. Green: the least code that makes it pass. Refactor: tidy the code while the tests stay green. Working this way is called **test-driven development**, or TDD. Watching the test fail first proves it can fail: a test that has never been red might not be testing anything at all.

## toBe or toEqual?

Two cinema tickets for the same seat in the same show would be a problem. They might look identical, but they're two pieces of paper. **`toBe`** asks "is it the very same thing?", which is the right question for single values such as numbers, text and `true`/`false`: 9 is 9. For an object or an array, it's the wrong question. The booth's price board, the sign above the window, is an object. Add this function to `tickets.js`:

```javascript
export function priceBoard(hour) {
  return {
    adult: ticketPrice(hour, 'adult'),
    student: ticketPrice(hour, 'student'),
    senior: ticketPrice(hour, 'senior'),
  };
}
```

A test with `expect(priceBoard(19)).toBe({ adult: 14, student: 12, senior: 11 })` fails, even though the prices are right:

```text
AssertionError: expected { adult: 14, student: 12, senior: 11 } to be { adult: 14, student: 12, senior: 11 } // Object.is equality

If it should pass with deep equality, replace "toBe" with "toStrictEqual"
```

The function made one object, and the test wrote a second object with the same contents: two tickets, not one. **`toEqual`** compares what's inside instead, every property and every item, so `expect(priceBoard(19)).toEqual({ adult: 14, student: 12, senior: 11 })` passes. (`toStrictEqual`, which the message suggests, is a stricter `toEqual` that also checks for properties that are `undefined`.)

The rule of thumb: **`toBe` for a single value, `toEqual` for an object or an array.**

## Watch mode

Back to the smoke alarm: it's always on. `npm test` works the same way. After the first run, Vitest stays open and waits, in what's called **watch mode**. Each time you save a file, it runs the tests again, in a moment, so you see red or green while the code is still on your screen. Press **h** for its list of keys, and **q** to quit.

To run the tests once and stop, the way an automatic check on GitHub does, use `npx vitest run`.

![A loop of three steps: red, a new test fails because the feature doesn't exist yet; green, the least code that makes it pass; refactor, tidy the code while every test stays green; then back to red for the next feature](https://fsdtrainingmedia.blob.core.windows.net/public/images/2026/10/56190be454bb4a079054800d75051b4f.webp)

**Try it.** Set up the `starlight-cinema` folder as above, with `tickets.js` and `tickets.test.js`, and run `npm test`. Leave it running. Change `hour < 17` to `hour < 13` in `tickets.js` and save: the matinee test turns red at once. Change it back. Then, test first, add a third guest type: children under 12 pay $5 less. Write the test, watch it fail, and make it pass.

## Check yourself

2 questions to try on the lesson page.

## So what

A test is a promise about your code that a machine checks for you, every time you save. Write the test first, and every feature arrives with its smoke alarm already on the ceiling.

## Key terms

- [Unit Test](https://www.ask-codey.com/glossary/unit-test-javascript)
- [Vitest](https://www.ask-codey.com/glossary/vitest-unit-testing)
- [describe, it and expect](https://www.ask-codey.com/glossary/describe-it-and-expect)
- [Assertion](https://www.ask-codey.com/glossary/assertion-unit-testing)
- [Test-Driven Development](https://www.ask-codey.com/glossary/test-driven-development)
- [Red, Green, Refactor](https://www.ask-codey.com/glossary/red-green-refactor)

## Cheat sheets

- [Your First Tests with Vitest](https://www.ask-codey.com/cheat-sheets/javascript-testing-first-tests-with-vitest-cheat-sheet)

## Related

- [Testing Edge Cases, Errors and Async Code](https://www.ask-codey.com/lessons/javascript-testing-edge-cases-errors-async)
- [Testing a Page with jsdom](https://www.ask-codey.com/lessons/javascript-testing-the-dom)

Source: https://www.ask-codey.com/lessons/javascript-testing-first-tests-with-vitest
