@dannysir/js-te
Jest-style test framework on top of Node native loader hooks
API Reference
Korean version: API.ko.md
A complete reference for every public API of @dannysir/js-te.
- Writing tests
- Matchers
expect(value).toBe(expected)expect(value).toEqual(expected)expect(fn).toThrow(matcher?)expect(value).toBeTruthy() / toBeFalsy()expect(value).toContain(item)expect(value).toBeInstanceOf(Class)expect(value).toBeNull() / toBeUndefined() / toBeDefined()expect(mockFn).toHaveBeenCalled() / toHaveBeenCalledWith(...args) / toHaveBeenCalledTimes(n).notchaining
- Mock Functions
- Module Mocking
- Browser entry
- How execution works
Writing tests
test(desc, fn)
Defines a single test.
test('array length', () => {
expect([1, 2, 3].length).toBe(3);
});describe(name, fn)
Groups tests. Can be nested.
describe('calculator', () => {
describe('add', () => {
test('positive numbers', () => {
expect(2 + 3).toBe(5);
});
});
describe('subtract', () => {
test('positive numbers', () => {
expect(5 - 3).toBe(2);
});
});
});beforeEach(fn)
Registers a function to run before every test. In nested describe blocks, outer beforeEach runs first, then the inner one.
describe('counter', () => {
let counter;
beforeEach(() => {
counter = 0;
});
test('increment', () => {
counter++;
expect(counter).toBe(1);
});
test('starts at 0', () => {
expect(counter).toBe(0);
});
describe('nested describe', () => {
beforeEach(() => {
counter = 10;
});
test('counter is 10', () => {
// outer beforeEach (0) β inner beforeEach (10)
expect(counter).toBe(10);
});
});
});test.each(cases)(template, fn)
Runs the same test once per row in cases. cases must be an array.
Placeholders
%sβ string / number%oβ object (rendered withJSON.stringify)
test.each([
[1, 2, 3, 6],
[3, 4, 5, 12],
[10, 20, 13, 43],
[10, 12, 13, 35],
])('[each test] - input : %s, %s, %s, %s', (a, b, c, result) => {
expect(a + b + c).toBe(result);
});
/* output
β [each test] - input : 1, 2, 3, 6
β [each test] - input : 3, 4, 5, 12
β [each test] - input : 10, 20, 13, 43
β [each test] - input : 10, 12, 13, 35
*/
test.each([
[{ name: 'dannysir', age: null }],
])('[each placeholder] - input : %o', (arg) => {
expect(arg.name).toBe('dannysir');
});
/* output
β [each placeholder] - input : {"name":"dannysir","age":null}
*/Matchers
expect(value).toBe(expected)
Compares with ===. Use for primitives like numbers and strings.
expect(5).toBe(5);
expect('hello').toBe('hello');expect(value).toEqual(expected)
Recursively compares the contents of objects and arrays. The comparison is key-order independent and safe against circular references.
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
expect([1, 2, 3]).toEqual([1, 2, 3]);
// equal regardless of key order
expect({ a: 1, b: 2 }).toEqual({ b: 2, a: 1 });
// circular references are handled without crashing
const a = { name: 'x' }; a.self = a;
const b = { name: 'x' }; b.self = b;
expect(a).toEqual(b);expect(fn).toThrow(matcher?)
Asserts that the function throws. The argument decides how the thrown error is checked.
| Argument | Meaning |
|---|---|
| (none) | Only verifies that a throw happened |
string | Error message contains the string |
RegExp | Error message matches the pattern |
Error subclass | instanceof check |
(err) => boolean | Predicate returns true for the error |
// throw or not
expect(() => { throw new Error('boom'); }).toThrow();
// substring
expect(() => { throw new Error('Something failed'); }).toThrow('failed');
// regex
expect(() => { throw new Error('code: 42'); }).toThrow(/code: \d+/);
// Error subclass
class CustomError extends Error {}
expect(() => { throw new CustomError(); }).toThrow(CustomError);
// predicate
expect(() => { throw new Error('boom'); }).toThrow((err) => err.message.length > 3);expect(value).toBeTruthy() / toBeFalsy()
Checks truthiness.
expect(true).toBeTruthy();
expect(0).toBeFalsy();expect(value).toContain(item)
Checks whether an array contains the item, or whether a string contains the substring.
expect([1, 2, 3]).toContain(2);
expect('hello world').toContain('world');expect(value).toBeInstanceOf(Class)
Checks value instanceof Class.
class Animal {}
class Dog extends Animal {}
expect(new Dog()).toBeInstanceOf(Dog);
expect(new Dog()).toBeInstanceOf(Animal);
expect([]).toBeInstanceOf(Array);expect(value).toBeNull() / toBeUndefined() / toBeDefined()
Strict checks for null / undefined.
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(0).toBeDefined(); // not undefined
expect(null).toBeDefined(); // null still counts as definedexpect(mockFn).toHaveBeenCalled() / toHaveBeenCalledWith(...args) / toHaveBeenCalledTimes(n)
Verifies the call history of a mock function created by fn(). Internally driven by mockFn.mock.calls.
const mockFn = fn();
mockFn(1, 2);
mockFn('hello');
expect(mockFn).toHaveBeenCalled(); // called at least once
expect(mockFn).toHaveBeenCalledTimes(2); // called exactly twice
expect(mockFn).toHaveBeenCalledWith(1, 2); // called with (1, 2) at some point
expect(mockFn).toHaveBeenCalledWith('hello');toHaveBeenCalledWith uses the same deep equal as toEqual to compare arguments, so it is key-order independent and safe against circular references.
.not chaining
Every matcher can be negated by chaining .not.
expect(5).not.toBe(6);
expect([1, 2, 3]).not.toContain(99);
expect(mockFn).not.toHaveBeenCalled();
expect(() => 'ok').not.toThrow();Mock Functions
A function returned by fn() is a mock function whose return value and implementation can be controlled at runtime β analogous to Jest's jest.fn().
fn(implementation?)
Creates a mockable function. Optionally takes an initial implementation.
import { fn } from '@dannysir/js-te';
test('mock function basics', () => {
const mockFn = fn();
mockFn('test');
mockFn(1, 2, 3);
// returns undefined by default
expect(mockFn()).toBe(undefined);
});
test('with initial implementation', () => {
const mockFn = fn((x, y) => x + y);
expect(mockFn(1, 2)).toBe(3);
});mockImplementation(fn)
Replaces the mock function's implementation.
test('change implementation', () => {
const mockFn = fn();
mockFn.mockImplementation((x) => x * 2);
expect(mockFn(5)).toBe(10);
});mockReturnValue(value)
Makes the mock always return the given value.
test('fixed return value', () => {
const mockFn = fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn()).toBe(42);
});mockReturnValueOnce(...values)
Queues up one-shot return values. After the queue is drained, the mock falls back to the previously-set value (or the default).
test('one-shot return values', () => {
const mockFn = fn();
mockFn.mockReturnValueOnce(1, 2, 3);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(3);
expect(mockFn()).toBe(undefined); // queue empty β default
});
test('mockReturnValueOnce + mockReturnValue', () => {
const mockFn = fn();
mockFn
.mockReturnValueOnce(1, 2)
.mockReturnValue(99);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(99); // 99 from now on
expect(mockFn()).toBe(99);
});mockClear()
Resets the mock's internal state (returnQueue, implementation, mock.calls, β¦).
test('reset mock state', () => {
const mockFn = fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockClear();
expect(mockFn()).toBe(undefined); // back to default
});mock.calls
Every call to a mock function appends its argument array to mockFn.mock.calls. The toHaveBeenCalledWith / toHaveBeenCalledTimes matchers use it under the hood, but you can also inspect it directly.
const mockFn = fn();
mockFn(1, 2);
mockFn('a', 'b', 'c');
mockFn.mock.calls; // [[1, 2], ['a', 'b', 'c']]
mockFn.mock.calls.length; // 2
mockFn.mock.calls[0]; // [1, 2]mockClear() also empties mock.calls.
Module Mocking
mock(path, mockObj)
Mocks a module. Both relative and absolute paths are supported, regardless of the order in which mock() and import are called.
mock()automatically converts every function in the module to a mock function.mock()returns the converted mock object β that returned object is the only way to callmockReturnValueand friends.
// math.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
// math.test.js
import { add, multiply } from './math.js';
test('using the returned mock object', () => {
const mockedMath = mock('./math.js', {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
});
// β
call mock function methods through the returned object
mockedMath.add.mockReturnValue(100);
mockedMath.multiply.mockReturnValueOnce(50, 75);
expect(add(1, 2)).toBe(100);
expect(multiply(2, 3)).toBe(50);
expect(multiply(2, 3)).toBe(75);
});Partial mocking
You can mock just some functions and let the rest fall through to the original implementation.
// calculator.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// calculator.test.js
import { add, subtract, multiply } from './calculator.js';
test('mock only multiply', () => {
const mocked = mock('./calculator.js', {
multiply: (a, b) => 999,
});
expect(add(2, 3)).toBe(5); // original
expect(subtract(5, 2)).toBe(3); // original
expect(multiply(2, 3)).toBe(999); // mocked
// dynamic control via mock function methods
mocked.multiply.mockReturnValue(100);
expect(multiply(2, 3)).toBe(100);
mocked.multiply.mockReturnValueOnce(50, 75);
expect(multiply(2, 3)).toBe(50);
expect(multiply(2, 3)).toBe(75);
expect(multiply(2, 3)).toBe(100); // back to the previous value
});Why must I use the returned object?
Short answer: the function you import is a wrapper function β it does not carry mock function methods.
mock() converts the module's exports into mock functions and stores them in mockStore, then returns that converted object. The function bound to your import is rewritten into a wrapper that consults mockStore on every call. The wrapper returns the right value, but it does not expose mockReturnValue and friends.
const mockedMath = mock('./math.js', {
add: (a, b) => a + b,
});
// β
mockedMath.add is the real mock function (has the methods)
mockedMath.add.mockReturnValue(100);
// β the imported `add` is a wrapper (no methods)
import { add } from './math.js';
// add.mockReturnValue(100); // TypeError: add.mockReturnValue is not a functionFor the full design of the wrapper pattern see λ‘λν κΈ°λ°μΈλ©λͺ¨λ¦¬λ³ν.md Β§ 4 (Korean).
ESM / CommonJS support
Both module systems are mocked the same way.
// random.js
export const random = () => Math.random();
// game.js (ESM)
import { random } from './random.js';
export const play = () => random() * 10;
// or game.js (CommonJS)
const { random } = require('./random.js');
module.exports.play = () => random() * 10;
// game.test.js
import { play } from './game.js';
test('mocking', () => {
const mocked = mock('./random.js', {
random: () => 0.5,
});
expect(play()).toBe(5);
mocked.random.mockReturnValue(0.3);
expect(play()).toBe(3);
mocked.random.mockReturnValueOnce(0.1);
expect(play()).toBe(1);
expect(play()).toBe(3); // back to the previous value
});clearAllMocks()
Removes every registered mock from mockStore.
Note:
clearAllMocks()only removes entries frommockStore. It does not reset each mock function's internal state (returnQueue,implementation,mock.calls, β¦). To reset that state, callmockClear()on the individual mock function.
unmock(path)
Removes a single mock by path.
isMocked(path)
Returns whether a path currently has a registered mock.
Browser entry
@dannysir/js-te/browser is a browser/Web Worker-safe entry that re-exports the pure test core. Use it when running test code directly in the browser (interactive demos, playgrounds), where the Node CLI runner can't run.
import { describe, test, expect, fn, beforeEach, testManager } from '@dannysir/js-te/browser';
describe('math', () => {
test('addition', () => {
expect(1 + 2).toBe(3);
});
});
await testManager.run();Exported: test (with test.each), describe, beforeEach, expect, fn, testManager.
Not exported: module mocking (mock, unmock, isMocked, clearAllMocks, mockStore) and the CLI runner (run). These depend on Node and are intentionally left out β the browser and Node entries differ on purpose.
testManager is a module-level singleton. If you collect tests more than once on the same page, call testManager.clearTests() between runs.
Node guard β importing this entry from a Node runtime throws immediately, pointing you to the main @dannysir/js-te entry (or the js-te CLI).
TypeScript β declarations ship with the package (types/browser.d.ts), so the entry is fully typed with no extra setup.
How execution works
Since 0.5.0 js-te runs through a load hook registered with Node's module.registerHooks() API.
- On startup the CLI parses each test file's AST and pre-collects only the paths passed to
mock(). registerHooks({ load })is installed.- Every subsequent
import/requireflows through the hook. Only files matching a pre-collected mock path get Babel-transformed, in memory only, before being handed back to Node. - The transformed code uses the wrapper pattern: each call consults
mockStoreat call time, so the result is correct regardless of the order betweenmock()andimport.
The user's source files are never modified on disk. See λ‘λν κΈ°λ°μΈλ©λͺ¨λ¦¬λ³ν.md (Korean) for the full design.