Unit testing
Learn unit testing with node:test, Vitest, Jest, clear assertions, Arrange-Act-Assert, and TDD by building a tiny JavaScript test runner.
- 01Write tests that say what code should doPick a small piece of code to test, give each test a name that reads like a sentence, and try normal, empty, and wrong input.
- 02Read assertions and test runnersUse strict equality, deep equality, and error checks, and read tests written for node:test, Vitest, and Jest.
- 03Write the test firstBuild a small feature in rounds: a failing test (red), the smallest fix (green), then a tidy-up (refactor).
Why code gets tests
A test is a small program that runs your code and checks the answer. It says what the code should do. Then it checks that the code really does it. A machine runs the tests for you, as often as you like.
Tests help you in four ways:
- Before you publish a change, they tell you the important parts still work.
- After you fix a bug, a test for that bug stops it from coming back. A bug that comes back is called a regression.
- They show other people what the code is supposed to do.
- They let you rewrite code more safely. Changing how code is written, without changing what it does, is called refactoring.
A unit is a small piece of code with one clear job that you can test quickly. It may be one function. It may also be a small file of code (a module) or one method of a class. The goal is quick feedback about one job.
function total(cart) {
return cart.reduce((sum, price) => sum + price, 0);
}
console.log(total([3, 5]));The total function adds the prices in the cart. It starts from zero. The last line passes two prices, 3 and 5, and prints 8.
A test for this function should check the answer, 8. It should not care whether the function uses a loop or reduce inside. Sometimes two functions work together to give callers one result. Then you can test the two of them together as one unit.
Unit testing means writing small automated tests for one unit at a time. Each test:
- has a name that says what the unit should do,
- gets the input ready,
- calls the unit once,
- checks the result.
Good unit tests run fast. They give the same result every time they run.
Pilots don't trust their memory for safety checks. Before the plane moves, they read a short checklist, one item at a time. Unit tests are that checklist for your code. They are quick, they run the same way every time, and each one checks one thing.
- In real life: Each checklist item names one thing to check
- In JavaScript: Each test name says one thing the code should do
- In real life: The crew checks the controls before takeoff
- In JavaScript: The tests check the code before you publish it
- In real life: A failed item stops the plane from leaving
- In JavaScript: A failing test stops a risky change
A project has other kinds of tests too. Each row in this table is one kind. Read across to see how many of them a project usually has and what they check.
| Kind of test | How many? | What does it check? |
|---|---|---|
| Unit tests | Many. | One small piece of code, like one function. They are fast and point to the exact job that broke. |
| Integration tests | Some. | A few real parts working together, like code that reads data and code that saves it. |
| UI and end-to-end tests | Few. | A whole task, done the way a user does it in a browser. They are slower and break more easily. |
| The testing trophy | A balance. | A different way to plan: put tests where they catch the most bugs for the least work, not only in one row. |
The next lessons on the Testing & debugging path cover fake helpers for tests (mocks and fakes), testing code that waits, testing pages in a real browser, and advanced debugging. If you want a refresher first, see pure functions, try/catch, error strategies, browser debugging, and npm scripts.
Build a tiny test runner
STEP THROUGHA test runner is a program that runs your tests and tells you which ones passed. Real runners are big, but the main loop is small:
- Collect the tests. Each test is a function with a name.
- Run each test function.
- Catch any test that fails.
- Print a result for every test.
You'll build a tiny runner first, so you can see each of these parts. Then the real tools will feel familiar.
The tiny runner has these functions:
test(name, fn)stores a test. It does not run it yet.equal,deepEqual, andthrowsare assertions. An assertion is a check. If the result is wrong, it throws an error.run()runs every stored test. It catches the errors, so one failing test doesn't stop the others. It reports a pass or a fail for each test.
First, a very small example of "store now, run later":
const tests = [];
tests.push(() => console.log("checked cart"));
console.log(tests.length);
tests[0]();Line 2 puts a function into the tests array. It doesn't call the function. So line 3 prints 1, and nothing else is printed yet. Line 4 calls the stored function, and it prints checked cart. A runner does the same thing, and also reports which tests failed.
Step through a tiny test runner. It stores the tests first. Then it runs each one and prints PASS or FAIL for it.
script
function createTinyRunner() { function test(name, fn) { cases.push({ name, fn }); } function equal(actual, expected, message) { if (!Object.is(actual, expected)) { throw new Error(message ?? `expected ${JSON.stringify(actual)} to equal ${JSON.stringify(expected)}`); } } function deepEqual(actual, expected, message) { if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error(message ?? `expected ${JSON.stringify(actual)} to deepEqual ${JSON.stringify(expected)}`); } } function throws(fn, messageIncludes) { try { fn(); } catch (error) { if (messageIncludes && !String(error.message).includes(messageIncludes)) { throw new Error(`expected error message to include ${messageIncludes}`); } return; } throw new Error("expected function to throw"); } function run() { return cases.map(({ name, fn }) => { try { fn(); return { name, status: "pass" }; } catch (error) { return { name, status: "fail", message: error.message }; } }); } return { test, equal, deepEqual, throws, run };} function cartTotal(items, code = "") { let total = 0; for (const item of items) { if (item.price < 0) throw new Error("negative prices are invalid"); total += item.price; } if (code === "SAVE10") return total; // bug: discount ignored return total;}function discountLabels(items) { return items.flatMap((item) => item.discounts ?? []);}function formatResults(results) { return results.map((result) => result.status === "pass" ? `PASS ${result.name}` : `FAIL ${result.name}: ${result.message}`).join("\n");} const { test, equal, deepEqual, throws, run } = createTinyRunner();test("totals item prices", () => { equal(cartTotal([{ name: "Book", price: 20 }, { name: "Pen", price: 5 }]), 25);});test("treats an empty cart as 0", () => { equal(cartTotal([]), 0);});test("collects discount labels", () => { deepEqual(discountLabels([{ name: "Book", price: 20, discounts: ["SAVE10"] }]), ["SAVE10"]);});test("rejects negative prices", () => { throws(() => cartTotal([{ name: "Bug", price: -1 }]), "negative");});test("applies SAVE10 discount", () => { equal(cartTotal([{ name: "Book", price: 20 }, { name: "Pen", price: 5 }], "SAVE10"), 15, "SAVE10 removes 10 dollars");}); console.log(formatResults(run()));The code starts with one bug: the cart ignores the discount code SAVE10. So one test fails. Its line says which job broke (applies SAVE10 discount) and gives the message SAVE10 removes 10 dollars. Choose Fixed discount and step through again. Now all five tests pass.
test() only stores each test in cases. run() calls them one by one, catches any failure, and prints one line per test.This runner is only for learning. Its deepEqual turns both values into JSON text and compares the text. That works for simple data. But two objects with the same properties in a different order give different text, so it would call them different. For real projects, use a real test runner.
Assertions say what must be true
TRY ITAn assertion is the part of a test that can fail. It compares the result your code gave (the actual value) with the result you want (the expected value). Different values need different checks:
- Strict equality checks that two values are exactly the same. Use it for numbers, strings, and true/false. For objects, it checks that both are the very same object.
- Deep equality checks arrays and objects part by part. Two separate objects pass if all their parts match.
- Error checks pass only when the code throws an error. Use them when bad input should throw.
Check one job in each test. Then a failing test points to one job.
const cart = { total: 8 };
const copy = { total: 8 };
console.log(Object.is(cart, copy));
console.log(cart.total === copy.total);cart and copy are two separate objects, so line 3 prints false. Object.is checks whether two things are exactly the same value, and two separate objects are never the same. Their total properties are both 8, so line 4 prints true. In a test, use assert.deepEqual when the whole object matters. Compare just one property when only that value matters.
The table lists the common assertions. Read each row as: what you want to check, how the code looks, and when to use it.
| What do you want to check? | How does the code look? | When do you use it? |
|---|---|---|
| Is it exactly the same value? | Object.is(a, b) or assert.equal from node:assert/strict. | For numbers, strings, true/false, null, and "is this the very same object?". |
| Do all the parts match? | assert.deepEqual or expect(value).toEqual(...). | For arrays and objects, when the parts matter and not which object it is. |
| Does it throw an error? | assert.throws(() => fn(), /message/). | For code that should throw an error on wrong input. |
| Does a promise fail? | await assert.rejects(promiseFn, /message/). | For promises that should fail (reject). The async testing lesson covers this. |
function equal(actual, expected) { if (!Object.is(actual, expected)) { throw new Error(`expected ${JSON.stringify(actual)} to strictly equal ${JSON.stringify(expected)}`); }}function check(name, fn) { try { fn(); return "PASS " + name; } catch (error) { return "FAIL " + name + ": " + error.message; }}console.log(check("0 is not false", () => equal(0, false)));FAIL 0 is not false: expected 0 to strictly equal falseA strict check doesn't convert values first. So it catches a mix-up like 0 and false, and this test fails.
assert.rejects, shown further down.A good test also tries unusual input, like an empty cart. Input at the edges like this is called an edge case.
function total(cart) {
return cart.reduce((sum, price) => sum + price, 0);
}
console.log(total([]));The last line passes an empty array and prints 0. That works because reduce starts from the 0 at the end of line 2. Without that starting value, reduce on an empty array throws an error. So the empty cart deserves its own test.
function total(cart) {
if (cart.some((price) => price < 0)) throw new RangeError("negative price");
return cart.reduce((sum, price) => sum + price, 0);
}
try { total([3, -1]); }
catch (error) { console.log(error instanceof RangeError, error.message); }Line 2 checks for a price below zero. The price -1 is below zero, so it throws a RangeError. The catch on line 6 prints true negative price. In a Node test you would write assert.throws(() => total([3, -1]), RangeError), or check the message.
Notice the () => in front. assert.throws needs a function that it can call itself. If you write total([3, -1]) directly, the error is thrown before assert.throws even starts.
Use strict checks in tests. Loose equality, ==, converts values before it compares them. For example, 0 == false is true. A strict check tells 0 and false apart. These are strict:
Object.is(actual, expected)assert.equalfromnode:assert/strictexpect(actual).toBe(expected)in Vitest and Jest
An error check should also look at the error's type or message. Otherwise, a test that throws for some other reason would still pass.
Some code returns a promise: a value that arrives later. If the promise fails (it rejects), you check that with assert.rejects. This code only runs in Node, so it has no Run button:
import assert from "node:assert/strict";
await assert.rejects(
() => loadUser("offline"),
/offline/,
"offline users should reject with a clear message",
);It works like assert.throws, but you must write await in front. Then the test waits for the promise to finish. The lesson on testing async code covers this in more detail.
Three steps in every test: arrange, act, assert
SORT ITMost tests have the same three steps. Keeping them in this order makes a test easy to read:
- Arrange: get the input and the expected answer ready.
- Act: call the code you are testing, once.
- Assert: check that the actual answer matches the expected answer.
Some teams say the same three steps as Given, When, Then: given this input, when I call the code, then I get this answer.
// Arrange: get the input and the expected answer ready.const cart = [{ price: 20 }, { price: 5 }];const expected = 25; // Act: run the code once.const actual = cart.reduce((total, item) => total + item.price, 0); // Assert: check the answer.console.log(actual === expected ? "PASS cart totals prices" : "FAIL cart totals prices");Lines 2 and 3 arrange: a cart with two prices, and the expected answer, 25. Line 6 acts: it adds the prices once. Line 9 asserts: it compares the two numbers and prints PASS cart totals prices.
Every test also needs a name. A good name reads like a sentence about what the code should do, such as "returns 0 for an empty cart". Avoid names like "line 7 works". Line numbers move when you rewrite the code, so the name would soon be wrong.
function total(cart) { return cart.reduce((sum, price) => sum + price, 0); }
const cart = []; // Arrange
const actual = total(cart); // Act
console.log(actual === 0 ? "PASS returns 0 for an empty cart" : "FAIL returns 0 for an empty cart"); // AssertLine 2 arranges an empty cart. Line 3 calls total once. Line 4 checks the answer and prints PASS returns 0 for an empty cart. In a real test file you would write test("returns 0 for an empty cart", () => ...). Then the runner prints that sentence if the test fails.
Another job, such as "rejects negative prices", goes in its own test. Don't add a second call to this one.
- Make a cart with two prices
Call `cartTotal(cart, "SAVE10")` onceCheck that the result is `15`Make `[{ price: -1 }]` to test wrong inputCall the function inside `assert.throws`Check that the error message has the word `negative`
Put each card under Arrange, Act, or Assert.
Test the edge cases before your users find them. Good ones to try:
- empty input, like an empty array,
- values right at a limit, like 0,
- the same value twice,
- wrong input, like a negative price,
- the smallest and largest values your code should handle.
A unit test is most useful when it catches a real mistake without a browser or a database.
Real test runners: node:test, Vitest, and Jest
COMPAREReal projects use a real test runner instead of a tiny one. Node.js has one built in, since Node 18. It comes in two parts:
node:testgives youtest,describe, anditto write tests.node:assert/strictgives you strict assertions, likeassert.equal.
You run the tests from a terminal with node --test. The code in this section is for test files in Node, so it has no Run button.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
function formatPrice(cents) {
if (!Number.isInteger(cents) || cents < 0) {
throw new RangeError("cents must be a non-negative integer");
}
return "$" + (cents / 100).toFixed(2);
}
describe("formatPrice", () => {
it("formats cents as dollars", () => {
assert.equal(formatPrice(1299), "$12.99");
});
it("rejects negative cents", () => {
assert.throws(() => formatPrice(-1), /non-negative/);
});
});describe on line 11 puts related tests into one group, named after the function. Each it inside it is one test. Line 13 checks a normal price. Line 17 checks that a negative price throws an error.
When you run this file, Node prints a report. The report uses a plain-text format called TAP. Each ok line is a test that passed, and the totals come at the end:
TAP version 13
# Subtest: formatPrice
# Subtest: formats cents as dollars
ok 1 - formats cents as dollars
# Subtest: rejects negative cents
ok 2 - rejects negative cents
1..2
ok 1 - formatPrice
1..1
# tests 2
# suites 1
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0This lesson's own tests really run the file above to get this report. The time each test took is removed, because it changes on every run. The pass and fail lines are real.
These commands are the ones you'll use most. Each line is one command:
node --testfinds all your test files and runs them.node --test tests/price.test.mjsruns just that one file.--watchruns the tests again each time you save a file (Node 22).--experimental-test-coverageshows which lines of your code the tests ran.--test-onlyruns only the tests marked with.only.
node --test
node --test tests/price.test.mjs
node --test --watch
node --test --experimental-test-coverage
node --test --test-onlyNode also lets you mark tests in a few special ways:
test.skipkeeps a test in the file but doesn't run it.test.todonotes a test you plan to write later.test.onlymarks the tests to run when you use--test-only.
import { describe, it, test } from "node:test";
import assert from "node:assert/strict";
describe("slugify", () => {
it("lowercases and hyphenates words", () => {
assert.equal(slugify("Hello Tests"), "hello-tests");
});
test.skip("documents a skipped behavior", () => {});
test.todo("supports non-Latin characters");
test.only("run this with node --test-only", () => {});
});This file expects a slugify function from your project. Remove .only when you're done. If you forget it, the other tests can stop running and you might not notice.
Two other runners are popular: Vitest and Jest. You install them from npm. The table compares all three. Read each row as: the runner, what it is, and which projects it suits.
| Runner | What is it? | Which projects does it suit? |
|---|---|---|
| node:test | It comes with Node 18 and newer. You check values with node:assert/strict and run the tests with node --test. | Small libraries, command-line tools, and projects that don't want to install a test tool. This website uses it. |
| Vitest | A test tool you install. It is made for Vite projects and checks values with expect, like Jest. It reruns tests quickly while you work. | Projects built with Vite, and newer projects that use import and TypeScript. |
| Jest | An older test tool you install. It has snapshots, mocks, a pretend web page (jsdom), and many plugins. | React projects that already use Jest, and projects that rely on its snapshots or plugins. |
Vitest is made for projects built with Vite, a popular build tool. It reads your files the same way Vite does. It checks values with expect, like Jest does. It can rerun tests quickly while you work. It can even run tests written inside the source file, using import.meta.vitest, after you turn on its includeSource setting.
Jest is older and has many plugins. It is known for:
- snapshots: it saves a copy of some output and warns you when the output changes,
- jsdom: a pretend web page inside Node, for testing page code,
- tools that convert TypeScript and newer syntax before the tests run.
import { describe, expect, test } from "vitest";
import { slugify } from "./slugify";
describe("slugify", () => {
test("lowercases and hyphenates words", () => {
expect(slugify("Hello Tests")).toBe("hello-tests");
});
});
// Vitest can also run tests written inside the source file:
if (import.meta.vitest) {
const { expect, test } = import.meta.vitest;
test("keeps numbers", () => {
expect(slugify("Release 22")).toBe("release-22");
});
}import { describe, expect, test } from "@jest/globals";
import { render } from "@testing-library/react";
test("button keeps its accessible name", () => {
const { container } = render(<button>Save</button>);
expect(container.firstChild).toMatchSnapshot();
});
describe("price math", () => {
test("formats cents", () => {
expect(formatPrice(1299)).toBe("$12.99");
});
});These two files need Vitest or Jest installed and set up in a project. The Jest file also needs a setup that understands JSX (the <button> tag), a pretend web page, and a formatPrice function from your project. Neither file runs in the browser editor.
This website's own tests use node:test. They live in the folder apps/client-ui/tests. Each file imports test from node:test and assertions from node:assert/strict. This lesson's own test file looks like this:
// apps/client-ui/tests/lesson-unit-testing.test.mjs
import assert from "node:assert/strict";
import { test } from "node:test";
import { article } from "../src/components/courses/javascript/unit-testing/unit-testing-lesson.ts";
test("the article definition is valid", () => {
assert.equal(article.curriculum?.lesson, "unit-testing");
});Write the test first: red, green, refactor
STEP THROUGHIn test-driven development (TDD), you write the test before the code. You work in small rounds. Each round has three steps:
- Red: write a test for the next thing the code should do. Run it and watch it fail.
- Green: make the smallest change to the code that makes the test pass.
- Refactor: tidy the code, like its names and repeated parts, while all tests keep passing.
Many test runners show failing tests in red and passing tests in green. That's where the names come from.
function total(cart, code = "") {
return cart.reduce((sum, price) => sum + price, 0);
}
const actual = total([20, 5], "SAVE10");
console.log(actual === 15 ? "PASS applies SAVE10" : "FAIL applies SAVE10");Line 2 adds up the prices, but nothing uses the discount code yet. Line 4 calls the function with SAVE10. The answer is 25, not 15, so line 5 prints FAIL applies SAVE10.
This is the right kind of red. The test fails because the discount is missing, not because of a typing mistake. Next, add the discount to the function and run the same test again. Only tidy the code after it passes.
TDD repeats three steps: write one failing test, make it pass with the smallest change, then tidy the code while the tests keep passing.
script
{ name: "trims spaces and joins words", input: " Hello Tests ", expected: "hello-tests" }, { name: "removes punctuation", input: "Red, green, refactor!", expected: "red-green-refactor" }, { name: "keeps numbers", input: "Release 22", expected: "release-22" },];const activeTests = allTests.slice(0, 1); function slugify(title) { return title;} function check({ name, input, expected }) { const actual = slugify(input); return actual === expected ? `PASS ${name}` : `FAIL ${name}: expected ${expected}, got ${actual}`;} console.log(activeTests.map(check).join("\n"));The lab builds a slugify function. It turns a title into a short web address part, like hello-tests. Pick a round and step through:
- red: there is one test. It fails, because
slugifyjust returns the title. - green: the first test now passes. A new test, for removing punctuation, fails. That is the red step of the next round.
- refactor: the punctuation test passes too, and a third test checks numbers. All three pass.
The lab squeezes two rounds into three views. In real TDD, you watch each new test fail before you write the code for it.
You can write tests before, during, or after the code. TDD works best when the next job is small and clear. If you are still trying out something new, write some quick try-out code first. Then turn what you learned into tests.
Testing in a real project
CHANGE A BUGIn a real project, unit tests help most when they check what the code gives back, not how it works inside. Good things to test:
- reading a price from text,
- making a slug from a title,
- picking the right discount,
- choosing the right error message for bad input,
- what a function promises to return to its callers.
A test should not fail just because you renamed a variable inside a function.
In the playground below, each button puts in a different bug. Watch which test fails.
function total(items, code = "") { if (items.length === 0) return 0; const sum = items.reduce((amount, item) => amount + item, 0); if (code === "SAVE10") return sum; // bug return sum;} const checks = [ { name: "empty cart is zero", actual: total([]), expected: 0 }, { name: "adds item prices", actual: total([20, 5]), expected: 25 }, { name: "SAVE10 removes ten dollars", actual: total([20, 5], "SAVE10"), expected: 15 },]; console.log(checks.map(({ name, actual, expected }) => actual === expected ? `PASS ${name}` : `FAIL ${name}: expected ${expected}, got ${actual}`).join("\n"));PASS empty cart is zeroPASS adds item pricesFAIL SAVE10 removes ten dollars: expected 15, got 25Only the discount test fails. Its name tells you which job broke, without reading the code first.
Write tests for the jobs you would worry about while rewriting the code. Say you change a loop into reduce, and a test breaks. First check whether the answer changed. If it didn't, the test knows too much about how the code works inside. But if a change to the discount math breaks the "SAVE10 removes ten dollars" test, the test did its job.
function totalWithLoop(cart) {
let sum = 0;
for (const price of cart) sum += price;
return sum;
}
function totalWithReduce(cart) { return cart.reduce((sum, price) => sum + price, 0); }
console.log(totalWithLoop([3, 5]) === totalWithReduce([3, 5]));totalWithLoop uses a for loop. totalWithReduce uses reduce. Both add the prices from zero, so line 7 prints true.
In a real test, check the expected total, 8, instead of comparing the two versions. Both versions could have the same mistake, and they would still agree. When a bug slips past your tests, add a test for it, and keep that test.
Common misconceptions
- “A unit is always one function.” A unit is a small piece of code with one clear job. Sometimes that is one function. Sometimes it is a small module that offers a few functions to the rest of the code.
- “More mocks make better unit tests.” A mock is a fake stand-in for a real part, like a pretend server. A few mocks help you test one part on its own. Too many mocks test how the parts are wired together, not what the code gives back. The next lesson covers mocks and fakes.
- “Snapshot tests are unit tests.” A snapshot test saves a copy of some output and warns you when it changes. It can catch a change in what a page shows. But a person still has to decide whether the change is right. Jest made snapshots popular. Use them with care.
- “One huge test gives more confidence.” When a small test fails, its message points to one job. Put one job in each test when you can.
- “If all tests pass, there are no bugs.” Tests only check the cases you wrote. When a bug slips through, add a test for it.
Unit tests, integration tests, and end-to-end tests are easy to mix up. In this table, each column is one kind of test. Read down a column to see what that kind checks.
| Question | Unit test | Integration test | End-to-end test |
|---|---|---|---|
| What does it check? | Does one job work? | Do a few real parts work together? | Can a user finish a task in the browser? |
| How fast is it? | Very fast. | Medium. | The slowest. |
| What does a failure tell you? | Which job is broken. | Where two parts don't fit together. | Which step of the user's task is broken. |
| Where can I learn more? | Right here, in this lesson. | The mocks and fakes lesson. | The UI and end-to-end testing lesson. |
Practice exercises
5 EXERCISESRead the code. Before you run it, type the name of the test that fails.
function total(items, code = "") {
const sum = items.reduce((amount, item) => amount + item, 0);
if (code === "SAVE10") return sum; // bug
return sum;
}
const checks = [
["empty cart is zero", total([]), 0],
["adds item prices", total([20, 5]), 25],
["SAVE10 removes ten dollars", total([20, 5], "SAVE10"), 15],
];
console.log(checks.filter(([, actual, expected]) => actual !== expected).map(([name]) => name).join(", "));The SAVE10 line returns the full sum, 25, instead of 15. So the test that fails is SAVE10 removes ten dollars.
This slugify has a bug. What kind of input should a new test use to show it?
function slugify(title) {
return title.toLowerCase().trim().replace(/\s+/g, "-");
}
const examples = ["Hello Tests", "", "Red, green!"];
console.log(examples.map(slugify).join("|"));Input with punctuation, such as Red, green!, shows the bug. The code returns red,-green!, but a slug should be red-green.
Type the three steps of a test, Arrange, Act, and Assert, in the right order.
const steps = ["Arrange", "Act", "Assert"];
console.log(steps.join(" -> "));The order is Arrange -> Act -> Assert. Given -> When -> Then means the same three steps.
Type the three TDD steps in order.
const loop = ["red", "green", "refactor"];
console.log(loop.join(" -> "));The order is red -> green -> refactor. Red shows the test can fail. Green shows the new code works. Refactor tidies the code while the tests keep passing.
Write the command that runs only the file tests/price.test.mjs with Node's built-in test runner.
const command = ["node", "--test", "tests/price.test.mjs"];
console.log(command.join(" "));node --test tests/price.test.mjs runs just that one file with Node's built-in test runner.
Check your understanding
8 QUESTIONSQuestion 1 of 8Which sentence best describes a unit test?
Choose an answer to see the explanation.
Question 2 of 8Which test name is most helpful?
Choose an answer to see the explanation.
Question 3 of 8What does this tiny assertion check print?
Read the code, then predictconst actual = 0; const expected = false; console.log(Object.is(actual, expected) ? "pass" : "fail");Choose an answer to see the explanation.
Question 4 of 8In Arrange-Act-Assert, which step is
const actual = slugify(input)?Choose an answer to see the explanation.
Question 5 of 8What does this edge-case snippet print?
Read the code, then predictfunction slugify(title) { return title.toLowerCase().trim().replace(/\s+/g, "-"); } console.log(slugify("Red, green!"));Choose an answer to see the explanation.
Question 6 of 8Which Node command finds your test files and runs them?
Choose an answer to see the explanation.
Question 7 of 8Why do many Vite projects pick Vitest?
Choose an answer to see the explanation.
Question 8 of 8What is the TDD order?
Read the code, then predictconst loop = ["red", "green", "refactor"]; console.log(loop.join(" -> "));Choose an answer to see the explanation.
Key takeaways
- A unit test runs one small piece of code and checks its answer. It runs fast, so you can run it often.
- A test runner stores your tests, runs each one, and reports which passed and which failed.
- An assertion is a check that throws an error when the answer is wrong. Pick strict equality, deep equality, throws, or rejects to match what you check.
- Arrange the input, act once, and assert the answer. Give each test a name that says what the code should do.
node:testcomes with Node. Vitest suits Vite projects. Jest is older and has many plugins.- TDD repeats red, green, refactor: a failing test, the smallest change that passes, then a tidy-up.
Remember the one-liner.
A good unit test is a short program that says what your code should do, and checks it.
Up next: mocks, spies, and fakes. They are stand-ins for things that are hard to test, like the clock, random numbers, saved data, and the network.