node:test has no expectation language of its own; assertions come from node:assert. Import the strict variant — node:assert/strict — and never the legacy default, whose equal uses == and whose deepEqual ignores prototypes, so '11' and 11 compare equal. In strict mode equal is Object.is-based, deepEqual is deepStrictEqual, and failures print the diff you just saw.
| Assertion | Use it for |
|---|---|
| equal, notEqual | primitives and identity |
| deepEqual, notDeepEqual | whole objects and arrays |
| partialDeepStrictEqual | an object with fields you do not control |
| ok(value) | a truthy check; prints the source expression |
| match, doesNotMatch | a string against a regular expression |
| throws, rejects | sync and async error paths |
partialDeepStrictEqual (Node 23.4.0, stable since 24.0.0) passes as long as the expected object is a subset of the actual one, which is what you want for an API response carrying an _id and a timestamp. assert.CallTracker went the other way, runtime-deprecated in Node 23 and removed in Node 24; use the mocking API in Mocking.
assert.throws takes the function itself, not its result, plus a second argument narrowing what may be thrown; assert.rejects is the same for promises and must be awaited. The context carries these as t.assert.*, which makes each assertion count toward the test's plan:
assert.throws(() => applyCoupon(30, 'NOPE'), {
name: 'Error', message: 'Unknown coupon: NOPE',
});
await assert.rejects(loadProduct('missing'), { code: 'ERR_NOT_FOUND' });
test('coupon rules', (t) => {
t.plan(2);
t.assert.equal(applyCoupon(30, 'HALF'), 15);
t.assert.throws(() => applyCoupon(30, 'NOPE'), /Unknown coupon/);
});A regular expression matches the message, an object compares the listed properties only, and a function may assert on anything but must return true. Forgetting await on assert.rejects writes a test that can never fail. t.plan(n) declares how many assertions must run, so a callback that silently never fires fails the test — the standard defense for event-driven code.