npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rcompat/test

v0.14.0

Published

Standard library testing

Readme

@rcompat/test

Testing library for JavaScript runtimes.

What is @rcompat/test?

A cross-runtime testing library with a fluent assertion API. Provides deep equality checks, type assertions, exception testing, fetch interception, and module mocking. Designed to work with the proby test runner. Works consistently across Node, Deno, and Bun.

Installation

npm install @rcompat/test
pnpm add @rcompat/test
yarn add @rcompat/test
bun add @rcompat/test

Usage

Writing tests

Create a .spec.ts or .spec.js file:

import test from "@rcompat/test";

test.case("addition works", assert => {
  assert(1 + 1).equals(2);
});

test.case("string concatenation", assert => {
  assert("hello" + " " + "world").equals("hello world");
});

Running tests

Use the proby test runner:

npx proby

Equality assertions

import test from "@rcompat/test";

test.case("equals", assert => {
  // primitives
  assert(42).equals(42);
  assert("hello").equals("hello");
  assert(true).equals(true);

  // objects (deep equality)
  assert({ a: 1, b: 2 }).equals({ a: 1, b: 2 });
  assert([1, 2, 3]).equals([1, 2, 3]);

  // nested structures
  assert({ users: [{ name: "Alice" }] }).equals({
    users: [{ name: "Alice" }],
  });
});

test.case("nequals", assert => {
  assert(1).nequals(2);
  assert("foo").nequals("bar");
  assert({ a: 1 }).nequals({ a: 2 });
});

Boolean assertions

import test from "@rcompat/test";

test.case("boolean checks", assert => {
  assert(1 === 1).true();
  assert(1 === 2).false();
  assert(null).null();
  assert(undefined).undefined();
});

Instance assertions

import test from "@rcompat/test";

test.case("instanceof", assert => {
  assert(new Date()).instance(Date);
  assert(new Map()).instance(Map);
  assert([1, 2, 3]).instance(Array);
});

Exception assertions

import test from "@rcompat/test";

test.case("throws", assert => {
  // check that function throws
  assert(() => {
    throw new Error("oops");
  }).throws("oops");

  // Check specific error type
  assert(() => {
    throw new TypeError("invalid input");
  }).throws(TypeError);
});

test.case("tries (does not throw)", assert => {
  assert(() => {
    return 42;
  }).tries();
});

Type assertions

import test from "@rcompat/test";

test.case("type checking", assert => {
  // compile-time type checks
  assert<string>().type<string>();
  assert("hello").type<string>();

  // check types don't match
  assert<string>().nottype<number>();
  assert(42).nottype<string>();

  // literal types
  assert<"foo">().type<"foo">();
  assert<"foo">().nottype<"bar">();
});

Async tests

import test from "@rcompat/test";

test.case("async operations", async assert => {
  const result = await Promise.resolve(42);
  assert(result).equals(42);
});

Cleanup with ended

import test from "@rcompat/test";

test.case("database test", async assert => {
  const db = await openDatabase();
  const user = await db.createUser({ name: "Alice" });
  assert(user.name).equals("Alice");
});

test.ended(async () => {
  // runs after all tests in this file
  await closeDatabase();
});

Grouping tests

Use test.group to cluster related test cases together. Groups can be run individually via proby.

import test from "@rcompat/test";

test.group("addition", () => {
  test.case("integers", assert => {
    assert(1 + 1).equals(2);
  });

  test.case("floats", assert => {
    assert(0.1 + 0.2).equals(0.3);
  });
});

test.group("subtraction", () => {
  test.case("integers", assert => {
    assert(3 - 1).equals(2);
  });
});

Run a specific group:

npx proby math.spec.ts addition

Mocking modules

Use test.mock to replace a module's exports and test.import to import the mocked module.

import test from "@rcompat/test";

using math = test.mock("./math.ts", () => ({
  add: (a, b) => 99,
}));

const { add } = await test.import("./math.ts");

test.case("returns mocked value", assert => {
  assert(add(1, 2)).equals(99);
});

test.case("tracks calls", assert => {
  add(1, 2);
  assert(math.add.calls.length).equals(1);
  assert(math.add.calls[0]).equals([1, 2]);
  assert(math.add.called).true();
});

Tracked mock functions record each call as a tuple of arguments in calls. Call tracking resets between test cases.

Static mocks with proby

When running tests with proby, you can preload mocks before a spec file is evaluated by adding a sibling mock file.

  • foo.spec.ts pairs with foo.mock.ts
  • foo.spec.js pairs with foo.mock.js

proby loads the mock file before the spec file, so the spec's static imports see the mocked module immediately.

// math.ts
export function add(a: number, b: number) {
  return a + b;
}
// math.mock.ts
import test from "@rcompat/test";

test.mock("./math.ts", () => ({
  add: (a: number, b: number) => 99,
}));
// math.spec.ts
import test from "@rcompat/test";
import { add } from "./math.ts";

test.case("static mock is loaded before the spec", assert => {
  assert(add(1, 2)).equals(99);
});

Static mocks are file-scoped when run through proby; they do not leak into later spec files.

Intercepting fetch

Use test.intercept to block outbound fetch calls to a specific origin and replace them with fake responses. Calls to other origins pass through untouched. The intercept records every request so you can assert on what was called and how.

import test from "@rcompat/test";

using telegram = test.intercept("https://api.telegram.org", setup => {
  setup.post("/sendMessage", () => ({
    ok: true,
    result: { message_id: 42 },
  }));
});

test.case("notifies user via telegram on signup", async assert => {
  await fetch("http://localhost:6161/signup", {
    method: "POST",
    body: JSON.stringify({ email: "[email protected]" }),
  });

  // assert the path was hit the right number of times
  assert(telegram.calls("/sendMessage")).equals(1);

  // assert on the actual request that came in
  assert(telegram.requests("/sendMessage")[0].method).equals("POST");
});

using restores the original fetch automatically when the file scope exits. For long-lived intercepts that need manual control, use restore() with test.ended:

const telegram = test.intercept("https://api.telegram.org", setup => {
  setup.post("/sendMessage", () => ({ ok: true, result: { message_id: 42 } }));
});

test.case("first case", async assert => {
  // ...
});

test.case("second case", async assert => {
  // ...
});

test.ended(() => telegram.restore());

Hitting a path on an intercepted origin that has no registered handler throws immediately, catching accidental unhandled calls early.

Extending the asserter

Use test.extend to attach custom assertion methods to the asserter. Useful for domain-specific assertions shared across many test cases.

import test from "@rcompat/test";

// create an extended test with custom assertions
const myTest = test.extend((assert, subject) => ({
  even() {
    const passed = subject % 2 === 0;
    // use the base asserter to report the result
    assert(passed).true();
    return this;
  },
}));

myTest.case("even numbers", assert => {
  assert(2).even();
  assert(4).even();
});

The factory receives the base assert function and the current subject (the value passed to assert()). Return an object whose methods will be mixed into every Assert instance for that test.

API Reference

test.case

test.case(name: string, body: (assert: Asserter) => void | Promise<void>): void;

Define a test case.

| Parameter | Type | Description | | --------- | ---------- | ------------------------------------- | | name | string | Test case name | | body | function | Test function receiving assert helper |

test.ended

test.ended(callback: () => void | Promise<void>): void;

Register a cleanup callback to run after all tests in the file.

test.group

test.group(name: string, fn: () => void): void;

Group test cases under a named scope. Groups can be targeted individually when running proby.

| Parameter | Type | Description | | --------- | ---------- | ----------------------------------- | | name | string | Group name, used by proby to filter | | fn | function | Function containing test.case calls |

test.mock

test.mock<T extends object>(
  specifier: string,
  factory: (original: unknown) => T,
): MockHandle<T>;

Register a module mock and return a handle to the tracked mocked exports. Function exports are wrapped so you can inspect calls and called.

| Parameter | Type | Description | | ----------- | ---------- | -------------------------------------------- | | specifier | string | Module specifier to mock | | factory | function | Returns the mocked exports for that module |

test.import

test.import(specifier: string): Promise<unknown>;

Import a module after mocks have been registered.

test.intercept

test.intercept(
  base_url: string,
  setup: (setup: Setup) => void
): Intercept;

Intercept outbound fetch calls to base_url. Returns an Intercept object for asserting on recorded requests.

| Parameter | Type | Description | | ---------- | ---------- | --------------------------------------------------------- | | base_url | string | Origin to intercept, e.g. "https://api.example.com" | | setup | function | Register route handlers on the setup object |

test.extend

test.extend<Subject, Extensions>(
  factory: (assert: Asserter, subject: Subject) => Extensions
): ExtendedTest<Extensions>;

Create a new test object with custom assertion methods mixed into the asserter.

| Parameter | Type | Description | | --------- | ---------- | ---------------------------------------------------------- | | factory | function | Returns extra methods to attach to each Assert instance |

Setup

| Method | Description | | ---------------------- | ------------------------- | | get(path, handler) | Register a GET handler | | post(path, handler) | Register a POST handler | | put(path, handler) | Register a PUT handler | | patch(path, handler) | Register a PATCH handler | | delete(path, handler)| Register a DELETE handler |

Each handler receives the incoming Request and returns a plain object, which is serialized into a Response automatically.

Intercept

| Method | Description | | ----------------------- | ----------------------------------------------- | | calls(path) | Number of times path was hit | | requests(path) | Array of Request objects recorded for path | | restore() | Reinstate the original globalThis.fetch | | [Symbol.dispose] | Called automatically by using |

Asserter

type Asserter = <T>(actual?: T) => Assert<T>;

The assert function passed to test cases.

Assert<T>

| Method | Description | | ----------------------- | ----------------------------------------- | | equals(expected) | Deep equality check | | nequals(expected) | Deep inequality check | | includes(expected) | Inclusion check (string, array, object) | | true() | Assert value is true | | false() | Assert value is false | | null() | Assert value is null | | undefined() | Assert value is undefined | | defined() | Assert value is not undefined | | instance(constructor) | Assert value is instance of class | | throws(expected?) | Assert function throws | | tries() | Assert function does not throw | | not | Negate the next assertion | | type<T>() | Compile-time type assertion | | nottype<T>() | Compile-time negative type assertion | | pass() | Manually pass the assertion | | fail(reason?) | Manually fail the assertion |

Utilities

equals

import equals from "@rcompat/test/equals";

equals(a: unknown, b: unknown): boolean;

Deep equality check supporting primitives, objects, arrays, maps, sets, dates.

any

import any from "@rcompat/test/any";

any(value: unknown): never;

Cast any value to never type for testing purposes.

undef

import undef from "@rcompat/test/undef";

const value: never = undef;

Pre-cast undefined value typed as never.

E

import E from "@rcompat/test/E";

E(error: unknown): { message: string };

Extract error data from unknown error types.

Examples

Testing a utility function

// sum.ts
export default (a, b) => a + b;

// sum.spec.ts
import test from "@rcompat/test";
import sum from "./sum.js";

test.case("sum adds two numbers", assert => {
  assert(sum(1, 2)).equals(3);
  assert(sum(-1, 1)).equals(0);
  assert(sum(0, 0)).equals(0);
});

Testing a class

import test from "@rcompat/test";

class Calculator {
  #value = 0;
  add(n) { this.#value += n; return this; }
  subtract(n) { this.#value -= n; return this; }
  get value() { return this.#value; }
}

test.case("calculator operations", assert => {
  const calc = new Calculator();
  calc.add(10).subtract(3);
  assert(calc.value).equals(7);
});

test.case("calculator is instance", assert => {
  assert(new Calculator()).instance(Calculator);
});

Testing error handling

import test from "@rcompat/test";

function divide(a, b) {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

test.case("divide throws on zero", assert => {
  assert(() => divide(10, 0)).throws("Division by zero");
});

test.case("divide works normally", assert => {
  assert(() => divide(10, 2)).tries();
  assert(divide(10, 2)).equals(5);
});

Testing code that calls external APIs

import test from "@rcompat/test";

using openai = test.intercept("https://api.openai.com", setup => {
  setup.post("/v1/chat/completions", () => ({
    choices: [{ message: { content: "Hello!" } }],
  }));
});

test.case("generates a reply", async assert => {
  const reply = await myService.generateReply("hi");
  assert(reply).equals("Hello!");
  assert(openai.calls("/v1/chat/completions")).equals(1);
});

Cross-Runtime Compatibility

| Runtime | Supported | | ------- | --------- | | Node.js | ✓ | | Deno | ✓ | | Bun | ✓ |

No configuration required — just import and use.

License

MIT

Contributing

See CONTRIBUTING.md in the repository root.