Test runner for JavaScript runtimes.
What is proby?#
A cross-runtime test runner that automatically discovers and runs test files.
Works with both single repositories and monorepos. Supports TypeScript source
execution in monorepos without a build step when your packages expose a custom
source condition in package.json. Uses @rcompat/test for writing tests.
Works consistently across Node, Deno, and Bun.
Installation#
npm install proby @rcompat/testpnpm add proby @rcompat/testyarn add proby @rcompat/testbun add proby @rcompat/testUsage#
Running tests#
Run proby from your project root:
npx probyRun a single file:
npx proby math.spec.tsRun a single group within a file:
npx proby math.spec.ts additionWhen invoking Proby through Deno, grant the permissions required by the test runner and the tests. Proby preserves full permissions when it relaunches Deno:
deno run -A node_modules/proby/lib/bin.jsHow proby resolves source files#
Proby reads compilerOptions.customConditions from your tsconfig.json and
relaunches itself with those conditions active before running tests. This lets
Node resolve package imports and exports to your TypeScript source files
instead of built JavaScript — no build step required.
Use a condition scoped to your project to avoid conflicts when multiple
monorepos share node_modules:
{
"compilerOptions": {
"customConditions": ["@myproject/source"]
}
}Then expose matching entries in your packages:
{
"imports": {
"#*": {
"@myproject/source": "./src/private/*.ts",
"default": "./lib/private/*.js"
}
},
"exports": {
".": {
"@myproject/source": "./src/public/index.ts",
"default": "./lib/public/index.js"
}
}
}Because TypeScript reads the same customConditions, your editor's
jump-to-source also lands on the .ts source file — not compiled output.
The opt-in is per package and entirely additive. Packages without the condition fall back to built output as normal.
Configuration#
Create spec.config.ts or spec.config.js in your project root. The preferred
helper is part of the test-authoring API, so the configuration contract does not
depend on Proby:
import test from "@rcompat/test";
export default test.config({
monorepo: true,
packages: "packages",
include: ["src"],
});A plain default-exported object is also valid. For type checking without the
helper, use the erased TestConfig type:
import type { TestConfig } from "@rcompat/test";
export default {
monorepo: true,
} satisfies TestConfig;Proby searches the project root for spec.config.ts, spec.config.js,
proby.config.ts, and proby.config.js. There is no silent filename
precedence: more than one supported file is an error, and the diagnostic lists
every match. With no configuration file, all defaults below apply.
The proby.config.* names and the default helper exported by proby remain
supported for existing projects. Migrate by renaming the file to
spec.config.* and changing import config from "proby" to
import test from "@rcompat/test", then replace config(...) with
test.config(...).
Configuration is imported by the active runtime. Current Node, Deno, and Bun
can load the erasable TypeScript used in these examples. Use
spec.config.js on a runtime that cannot load TypeScript directly; Proby does
not transpile configuration files.
Config options#
monorepo#
booleanWhether proby should scan package directories inside a monorepo.
Default: false
packages#
stringDirectory containing package folders when monorepo is enabled.
The path is resolved from the project root.
Default: "packages"
include#
string[]Directories to scan for spec files.
For a single project, each path is resolved from the project root. In monorepo
mode, each path is resolved from every directory inside packages.
Default: ["src", "test"]
loaders#
string[]Modules to preload before tests run. Paths are resolved from the project root and passed directly to the active runtime.
Default: []
Project structure#
Single repository#
my-project/
├── src/
│ ├── utils.ts
│ ├── utils.spec.ts
│ ├── math.ts
│ └── math.spec.ts
└── package.jsonMonorepo#
my-monorepo/
├── packages/
│ ├── core/
│ │ └── src/
│ │ ├── index.ts
│ │ └── index.spec.ts
│ └── utils/
│ └── src/
│ ├── helpers.ts
│ └── helpers.spec.ts
└── package.jsonTest file conventions#
- Files must end with
.spec.tsor.spec.js - Files must be in one of the configured include directories
- Use
@rcompat/testto write tests - Use
test.groupto organize cases into named groups targetable by proby
Static mock files#
Proby supports preloading sibling mock files before a spec file is evaluated. This allows module mocks to be registered before the spec's static imports are resolved.
Pair files by matching the spec extension exactly:
math.spec.tspairs withmath.mock.tsmath.spec.jspairs withmath.mock.js
If a sibling mock file exists, proby loads it before the spec file.
// 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("uses the preloaded mock", assert => {
assert(add(1, 2)).equals(99);
});Static mocks are file-scoped — they do not leak into later spec files.
Grouping tests#
Use test.group to cluster related cases. Groups can be targeted individually
when running proby:
import test from "@rcompat/test";
test.group("addition", () => {
test.case("integers", assert => {
assert(1 + 1).equals(2);
});
});
test.group("multiplication", () => {
test.case("integers", assert => {
assert(2 * 3).equals(6);
});
});npx proby math.spec.ts additionWriting tests#
Create test files with .spec.ts or .spec.js:
import test from "@rcompat/test";
test.case("addition", assert => {
assert(1 + 1).equals(2);
assert(2 + 2).equals(4);
});
test.case("multiplication", assert => {
assert(2 * 3).equals(6);
assert(4 * 5).equals(20);
});Test output#
Proby displays colored output:
- Green
ofor passing tests - Red
xfor failing tests
oooooxoo
src/math.spec.ts division
expected 5
actual 4npm scripts#
Add proby to your package.json:
{
"scripts": {
"test": "npx proby"
}
}Then run:
npm testExamples#
Basic assertions#
import test from "@rcompat/test";
test.case("basic assertions", assert => {
assert(value).equals(expected);
assert(condition).true();
assert(condition).false();
assert(value).type<string>();
assert(() => throwingFunction()).throws();
assert(() => safeFunction()).tries();
});Async tests#
import test from "@rcompat/test";
test.case("async operations", async assert => {
const result = await fetchData();
assert(result.status).equals(200);
});Testing modules#
// src/calculator.ts
export function add(a: number, b: number) {
return a + b;
}// src/calculator.spec.ts
import test from "@rcompat/test";
import { add } from "./calculator.js";
test.case("add function", assert => {
assert(add(1, 2)).equals(3);
assert(add(-1, 1)).equals(0);
assert(add(0, 0)).equals(0);
});Cross-Runtime Compatibility#
| Runtime | Supported |
|---|---|
| Node.js | ✓ |
| Deno | ✓ |
| Bun | ✓ |
License#
MIT
Contributing#
See CONTRIBUTING.md in the repository root.