A minimal, focused testing framework for writing tests that run identically across Deno, Bun, and Node.js. Part of the @cross suite - check out our growing collection of cross-runtime tools at github.com/cross-org.
While node:test now works across runtimes, @cross/test provides unique advantages:
- Unified Simple API - Single
test()function with consistent behavior across all runtimes - JSR-First - Seamlessly works with JSR packages like
@std/assertand@std/expect - Test Steps - Built-in
context.step()support for organizing tests into sequential steps with shared state - Callback Support - Native
waitForCallbackoption for callback-based async tests - Minimal Surface - Focused API that abstracts runtime differences without bloat
Install @cross/test along with the assertion library of your choice. We recommend using @std/assert for consistency across runtimes:
# Pick your runtime and package manager:
npx jsr add @cross/test @std/assert # Node.js
deno add jsr:@cross/test jsr:@std/assert # Deno
bunx jsr add @cross/test @std/assert # Bunimport { test } from "@cross/test";
import { assertEquals, assertNotEquals } from "@std/assert";
test("Multiplication", () => {
assertEquals(5 * 4, 20);
});
test("Test with timeout", () => {
assertEquals(5 * 4, 20);
}, { timeout: 1000 });
// Callback-based async test (unique to @cross/test)
test("Callback-based async", (_context, done) => {
setTimeout(() => {
assertNotEquals(5, 4);
done();
}, 500);
}, { waitForCallback: true });Organize tests into steps with shared state - perfect for integration tests and workflows:
import { test } from "@cross/test";
import { assertEquals } from "@std/assert";
test("User registration flow", async (context) => {
let userId;
await context.step("Create user", () => {
userId = createUser("[email protected]");
assertEquals(typeof userId, "string");
});
await context.step("Verify user exists", () => {
const user = getUser(userId);
assertEquals(user.email, "[email protected]");
});
await context.step("Delete user", () => {
deleteUser(userId);
assertEquals(getUser(userId), null);
});
});Steps share the parent test's scope and execute sequentially, making complex test flows easy to write and debug.
Just like tests, steps can also use callbacks for async operations with the waitForCallback option:
import { test } from "@cross/test";
import { assertEquals } from "@std/assert";
test("Test with callback-based steps", async (context) => {
let completed = false;
await context.step("Async operation", (_context, done) => {
setTimeout(() => {
completed = true;
done();
}, 100);
}, { waitForCallback: true });
assertEquals(completed, true);
});import { test } from "@cross/test";
import { assertEquals } from "@std/assert";
import sinon from "sinon";
// Prepare the "environment"
function bar() {/*...*/}
export const funcs = {
bar,
};
export function foo() {
funcs.bar();
}
test("calls bar during execution of foo", () => {
const spy = sinon.spy(funcs, "bar");
foo();
assertEquals(spy.called, true);
assertEquals(spy.getCalls().length, 1);
});- Node.js:
node --test - Node.js (TS):
npx tsx --testRemember{ "type": "module" }in package.json - Deno:
deno test - Bun:
bun test
- Bun (GitHub Actions):
name: Bun CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: antongolub/[email protected]
with:
bun-version: v1.x # Uses latest bun 1
- run: bun x jsr add @cross/test @std/assert # Installs dependencies
- run: bun test # Runs the tests- Deno (GitHub Actions):
name: Deno CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x # Uses latest deno version 1
- run: deno add @cross/test @std/assert # Installs dependencies from jsr.io
- run: deno test # Runs tests- Node (GitHub actions):
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 21.x]
steps:
- uses: actions/checkout@v3
- run: npx jsr add @cross/test @std/assert
- run: "echo '{ \"type\": \"module\" }' > package.json" # Needed for tsx to work
- run: npx --yes tsx --test *.test.ts