testing-guidelines
Node.js native test runner patterns — assertions, test structure, async tests, isolation, and mocking with node:test and node:assert/strict.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Node.js native test runner patterns — assertions, test structure, async tests, isolation, and mocking with node:test and node:assert/strict.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Security guidelines for TypeScript and Node.js — input validation, injection prevention, authentication hardening, secrets management, HTTP security headers, and common vulnerability patterns
In-memory caching patterns for TypeScript — memoization, async deduplication, TTL, LRU, stale-while-revalidate, and invalidation strategies
Concrete patterns for composing behavior from small units — function composition, middleware, decorators, mixins, monad chaining, builders, plugin systems, strategy composition, and type-level composition for TypeScript
ESM-specific patterns for TypeScript and Node.js — file extensions, import.meta, dynamic imports, dual packages, and module resolution modes
Guidelines for designing and implementing HTTP REST APIs — resource naming, HTTP semantics, status codes, error responses, pagination, and versioning
Production-ready Playwright patterns — config, locators, assertions, page objects, network mocking, and CI setup for reliable E2E tests.
| name | testing-guidelines |
| description | Node.js native test runner patterns — assertions, test structure, async tests, isolation, and mocking with node:test and node:assert/strict. |
Write tests that verify behavior, catch regressions, and stay cheap to maintain. This skill covers the Node.js native test runner only (node:test + node:assert/strict).
Run tests with tsx so TypeScript files execute without a compile step:
{
"scripts": {
"test": "tsx --test 'src/**/*.test.ts'"
}
}
Pass a glob; tsx --test discovers files matching it. For a single file:
tsx --test src/math.test.ts
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { myFn } from "./math.js";
node:test — never from a third-party test framework.node:assert/strict as the default import; it enables strict mode for all assertions..js extensions on relative imports even when source files are .ts.| Assertion | Use for |
|---|---|
assert.strictEqual(actual, expected) | Primitives, reference equality (===) |
assert.deepStrictEqual(actual, expected) | Objects, arrays, Maps, Sets |
assert.throws(fn, /pattern/) | Synchronous throws; optionally match message |
assert.rejects(asyncFn, /pattern/) | Promise rejections |
assert.match(str, /pattern/) | Regex match on strings |
assert.ok(value) | Truthiness — prefer a more specific assertion when possible |
Provide the expected value second; the error message will read naturally.
// ✅ clear failure message: "Expected '3' to equal '4'"
assert.strictEqual(add(1, 2), 3);
// ✅ deep equality for objects
assert.deepStrictEqual(parse("a=1&b=2"), { a: "1", b: "2" });
// ✅ throws with message check
assert.throws(() => divide(1, 0), /division by zero/i);
// ✅ async rejection
await assert.rejects(() => fetchUser(-1), /invalid id/i);
When the same behavior is exercised with multiple inputs, use a testcases array and iterate — never copy-paste test blocks.
const testcases = [
{ input: "hello world", expected: "Hello World" },
{ input: "already Cased", expected: "Already Cased" },
{ input: "", expected: "" },
];
for (const { input, expected } of testcases) {
it(`titleCase("${input}") === "${expected}"`, () => {
assert.strictEqual(titleCase(input), expected);
});
}
Rules:
A test earns its place when it:
"returns empty array when input has no matches".node:test supports async test functions natively. await inside the test body is all you need.
it("resolves with user data for a valid id", async () => {
const user = await getUser(1);
assert.deepStrictEqual(user, { id: 1, name: "Alice" });
});
it("rejects with NOT_FOUND for an unknown id", async () => {
await assert.rejects(() => getUser(999), /not found/i);
});
Test both the success and the failure path of every async operation that can fail.
describe("fetchPrices", () => {
it("returns prices array on success", async () => {
const prices = await fetchPrices("EUR");
assert.ok(Array.isArray(prices));
});
it("rejects when currency code is invalid", async () => {
await assert.rejects(() => fetchPrices("XXX"), /unsupported currency/i);
});
});
Shared mutable state between tests is a reliability hazard. Tests must not depend on execution order.
import { beforeEach } from "node:test";
let db: Database;
beforeEach(() => {
// fresh instance before every test — no state leaks
db = new Database(":memory:");
db.seed(fixtures);
});
Rules:
beforeEach.afterEach or after to close connections, clear timers, restore mocks.beforeEach; avoid sharing objects across test cases.Mock I/O boundaries and non-deterministic dependencies. Do not mock pure functions.
Mock these:
Date.now() / Math.random() / clocksDo not mock these:
import { mock } from "node:test";
import type { Logger } from "../src/logger.js";
it("calls logger.warn when retry limit is exceeded", () => {
const warn = mock.fn<Logger["warn"]>();
const logger: Logger = { warn, info: mock.fn(), error: mock.fn() };
runWithRetry({ attempts: 0, maxAttempts: 3, logger });
assert.strictEqual(warn.mock.calls.length, 1);
assert.match(warn.mock.calls[0]?.arguments[0] as string, /retry limit/i);
});
Restore mocks after each test to avoid state leakage:
afterEach(() => mock.restoreAll());
For time-dependent code, mock the clock rather than calling Date.now() directly:
it("expires a token after 60 seconds", () => {
const clock = mock.timers;
clock.enable({ apis: ["Date"] });
const token = createToken();
clock.tick(61_000);
assert.strictEqual(isExpired(token), true);
clock.reset();
});
| Hook | Runs |
|---|---|
before | Once before all tests in the current describe |
after | Once after all tests in the current describe |
beforeEach | Before every individual it in the current describe |
afterEach | After every individual it in the current describe |
Keep before/after for expensive one-time setup (starting a test server, opening a DB connection). Use beforeEach/afterEach for per-test state reset.
describe("UserRepository", () => {
let repo: UserRepository;
before(async () => {
await db.migrate(); // run once
});
beforeEach(() => {
repo = new UserRepository(db); // fresh instance per test
db.truncate("users");
});
after(async () => {
await db.close();
});
});
| Mistake | Fix |
|---|---|
Importing from node:assert instead of node:assert/strict | Use import assert from "node:assert/strict" — loose mode hides type mismatches |
| Duplicating test structure for each input variant | Use a testcases array and loop |
| Asserting on implementation internals (which function was called, which branch ran) | Assert on the return value or observable side effect |
Forgetting await on assert.rejects | Without await the assertion always passes even if the function throws nothing |
| Shared mutable fixtures at module scope without reset | Move setup into beforeEach |
| Mocking pure functions | Pure functions are fast and deterministic — test through them, not around them |
Not calling mock.restoreAll() after mocking | Mocked methods persist across tests and cause false passes or failures |
| Writing a test per function instead of per behavior | One function may need multiple tests; one test may cover multiple functions |
Using .js extension on bare specifiers | Only relative imports need extensions — node:test and npm packages do not |