Node ships its own test runner built in — node:test — so a first real test needs no extra package at all. (Popular external runners like Jest and Vitest add extra features on top of the same core idea, worth exploring later.)
// math.test.js
import { test } from 'node:test'
import assert from 'node:assert'
function add(a, b) {
return a + b
}
test('add() sums two numbers', () => {
assert.strictEqual(add(2, 3), 5)
})node --test
# ✔ add() sums two numbersimport { describe, test } from 'node:test'
import assert from 'node:assert'
describe('add()', () => {
test('sums two positive numbers', () => {
assert.strictEqual(add(2, 3), 5)
})
test('handles negative numbers', () => {
assert.strictEqual(add(-2, 3), 1)
})
})test('fetches a user', async () => {
const user = await getUser(1)
assert.strictEqual(user.name, 'Sam')
})Rather than starting a real server on a real port, a request library that can call an Express app directly (like supertest) tests routes without any network involved.
import { test } from 'node:test'
import assert from 'node:assert'
import request from 'supertest'
import app from './app.js'
test('GET /products returns a list', async () => {
const response = await request(app).get('/products')
assert.strictEqual(response.status, 200)
assert.ok(Array.isArray(response.body))
})| Method | Checks |
|---|---|
| assert.strictEqual(a, b) | a === b |
| assert.deepStrictEqual(a, b) | a and b have the same structure/values (for objects and arrays) |
| assert.ok(value) | value is truthy |
| assert.throws(fn) | Calling fn throws an error |
| assert.rejects(promise) | An async function's returned promise rejects |
Test the failure cases too
A test suite that only ever exercises the "happy path" (valid input, success case) misses most real bugs — deliberately write at least one test for what should happen with bad input, a missing field, or an error thrown deep in the code.