-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(test): export assert from bun:test #25130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7496e89
8e67b3a
0c42652
2f62188
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| --- | ||
| title: Use assert in the Bun test runner | ||
| sidebarTitle: assert | ||
| mode: center | ||
| --- | ||
|
|
||
| Bun's test runner exports `assert` from `bun:test`, which is a re-export of Node.js's `node:assert` module. This provides vitest-style assertions alongside Jest-style `expect()`. | ||
|
|
||
| ```ts test.ts icon="/icons/typescript.svg" | ||
| import { test, assert } from "bun:test"; | ||
|
|
||
| test("basic assertion", () => { | ||
| assert(true); | ||
| assert(1 + 1 === 2, "math should work"); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Type narrowing | ||
|
|
||
| The `assert` function works as a TypeScript type guard, narrowing types after assertion: | ||
|
|
||
| ```ts test.ts icon="/icons/typescript.svg" | ||
| import { test, assert } from "bun:test"; | ||
|
|
||
| test("type narrowing", () => { | ||
| const value: string | null = getValue(); | ||
| assert(value !== null); | ||
| // TypeScript now knows value is string | ||
| console.log(value.toUpperCase()); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Available methods | ||
|
|
||
| Since `assert` is a re-export of `node:assert`, all Node.js assertion methods are available: | ||
|
|
||
| ```ts test.ts icon="/icons/typescript.svg" | ||
| import { test, assert } from "bun:test"; | ||
|
|
||
| test("equality checks", () => { | ||
| assert.strictEqual(1, 1); | ||
| assert.deepStrictEqual({ a: 1 }, { a: 1 }); | ||
| assert.notStrictEqual(1, 2); | ||
| }); | ||
|
|
||
| test("error handling", () => { | ||
| assert.throws(() => { | ||
| throw new Error("expected"); | ||
| }); | ||
|
|
||
| assert.doesNotThrow(() => { | ||
| // safe code | ||
| }); | ||
| }); | ||
|
|
||
| test("async assertions", async () => { | ||
| await assert.rejects(async () => { | ||
| throw new Error("async error"); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Using with expect | ||
|
|
||
| You can use `assert` alongside Bun's `expect()` in the same test: | ||
|
|
||
| ```ts test.ts icon="/icons/typescript.svg" | ||
| import { test, expect, assert } from "bun:test"; | ||
|
|
||
| test("mixed assertions", () => { | ||
| const result = compute(); | ||
|
|
||
| // Use assert for type narrowing | ||
| assert(result !== null); | ||
|
|
||
| // Use expect for rich matchers | ||
| expect(result.value).toBeGreaterThan(0); | ||
| expect(result.items).toContain("expected"); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| See also: | ||
|
|
||
| - [Node.js assert documentation](https://nodejs.org/api/assert.html) | ||
| - [Docs > Test runner > Writing tests](/test/writing-tests) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import { assert, expect, test } from "bun:test"; | ||
|
|
||
| test("assert is exported from bun:test", () => { | ||
| expect(typeof assert).toBe("function"); | ||
| }); | ||
|
|
||
| test("assert(condition) works", () => { | ||
| assert(true); | ||
| assert(1); | ||
| assert("non-empty string"); | ||
|
|
||
| expect(() => assert(false)).toThrow(); | ||
| expect(() => assert(0)).toThrow(); | ||
| expect(() => assert("")).toThrow(); | ||
| }); | ||
|
|
||
| test("assert with message works", () => { | ||
| assert(true, "should not throw"); | ||
|
|
||
| expect(() => assert(false, "custom error message")).toThrow(/custom error message/); | ||
| }); | ||
|
|
||
| test("assert.ok works", () => { | ||
| assert.ok(true); | ||
| assert.ok(1); | ||
|
|
||
| expect(() => assert.ok(false)).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.strictEqual works", () => { | ||
| assert.strictEqual(1, 1); | ||
| assert.strictEqual("hello", "hello"); | ||
|
|
||
| expect(() => assert.strictEqual(1, 2)).toThrow(); | ||
| expect(() => assert.strictEqual(1, "1")).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.deepStrictEqual works", () => { | ||
| assert.deepStrictEqual({ a: 1 }, { a: 1 }); | ||
| assert.deepStrictEqual([1, 2, 3], [1, 2, 3]); | ||
|
|
||
| expect(() => assert.deepStrictEqual({ a: 1 }, { a: 2 })).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.notStrictEqual works", () => { | ||
| assert.notStrictEqual(1, 2); | ||
| assert.notStrictEqual(1, "1"); | ||
|
|
||
| expect(() => assert.notStrictEqual(1, 1)).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.throws works", () => { | ||
| assert.throws(() => { | ||
| throw new Error("test error"); | ||
| }); | ||
|
|
||
| expect(() => | ||
| assert.throws(() => { | ||
| // does not throw | ||
| }), | ||
| ).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.doesNotThrow works", () => { | ||
| assert.doesNotThrow(() => { | ||
| // does not throw | ||
| }); | ||
|
|
||
| expect(() => | ||
| assert.doesNotThrow(() => { | ||
| throw new Error("test error"); | ||
| }), | ||
| ).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.rejects works", async () => { | ||
| await assert.rejects(async () => { | ||
| throw new Error("async error"); | ||
| }); | ||
|
|
||
| await expect( | ||
| assert.rejects(async () => { | ||
| // does not reject | ||
| }), | ||
| ).rejects.toThrow(); | ||
| }); | ||
|
|
||
| test("assert.doesNotReject works", async () => { | ||
| await assert.doesNotReject(async () => { | ||
| // does not reject | ||
| }); | ||
|
|
||
| await expect( | ||
| assert.doesNotReject(async () => { | ||
| throw new Error("async error"); | ||
| }), | ||
| ).rejects.toThrow(); | ||
| }); | ||
|
|
||
| test("assert.equal works (loose equality)", () => { | ||
| assert.equal(1, 1); | ||
| assert.equal(1, "1"); // loose equality allows this | ||
|
|
||
| expect(() => assert.equal(1, 2)).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.notEqual works (loose inequality)", () => { | ||
| assert.notEqual(1, 2); | ||
|
|
||
| expect(() => assert.notEqual(1, 1)).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.deepEqual works", () => { | ||
| assert.deepEqual({ a: 1 }, { a: 1 }); | ||
| assert.deepEqual([1, 2], [1, 2]); | ||
|
|
||
| expect(() => assert.deepEqual({ a: 1 }, { a: 2 })).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.notDeepEqual works", () => { | ||
| assert.notDeepEqual({ a: 1 }, { a: 2 }); | ||
|
|
||
| expect(() => assert.notDeepEqual({ a: 1 }, { a: 1 })).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.fail works", () => { | ||
| expect(() => assert.fail()).toThrow(); | ||
| expect(() => assert.fail("custom message")).toThrow(/custom message/); | ||
| }); | ||
|
|
||
| test("assert.ifError works", () => { | ||
| assert.ifError(null); | ||
| assert.ifError(undefined); | ||
|
|
||
| expect(() => assert.ifError(new Error("test"))).toThrow(); | ||
| expect(() => assert.ifError("some error")).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.match works", () => { | ||
| assert.match("hello world", /world/); | ||
|
|
||
| expect(() => assert.match("hello", /world/)).toThrow(); | ||
| }); | ||
|
|
||
| test("assert.doesNotMatch works", () => { | ||
| assert.doesNotMatch("hello", /world/); | ||
|
|
||
| expect(() => assert.doesNotMatch("hello world", /world/)).toThrow(); | ||
| }); | ||
|
Comment on lines
+1
to
+149
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Comprehensive coverage of the These tests do a solid job of exercising the If you want to further lock in the “re‑export” guarantee (not just behavioral similarity), you could add a small identity check: -import { assert, expect, test } from "bun:test";
+import { assert, expect, test } from "bun:test";
+import nodeAssert from "node:assert";
+
+test("assert is the node:assert export", () => {
+ expect(assert).toBe(nodeAssert);
+});Optional, but it would guarantee that 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Guard lazy test module initializer with a ThrowScope when loading
node:assertInside
m_lazyTestModuleObject.initLateryou now:internalModuleRegistry()->requireId(..., Field::NodeAssert).putDirectthe returned value as"assert".Unlike the nearby
m_utilInspectFunctioninitializer, this path doesn’t useDECLARE_THROW_SCOPE/RETURN_IF_EXCEPTION, so any unexpected exception fromBun__Jest__createTestModuleObject,requireId, orputDirectwould escape without a controlled early return from the initializer.You can align this with existing patterns and make it more robust with a small change:
m_lazyTestModuleObject.initLater( [](const Initializer<JSObject>& init) { - auto* globalObject = jsCast<Zig::GlobalObject*>(init.owner); - auto& vm = init.vm; - - JSValue result = JSValue::decode(Bun__Jest__createTestModuleObject(globalObject)); - JSObject* testModule = result.toObject(globalObject); - - // Add node:assert as "assert" export for vitest compatibility - JSValue assertModule = globalObject->internalModuleRegistry()->requireId(globalObject, vm, Bun::InternalModuleRegistry::Field::NodeAssert); - if (assertModule && !assertModule.isUndefinedOrNull()) { - testModule->putDirect(vm, Identifier::fromString(vm, "assert"_s), assertModule); - } - - init.set(testModule); + auto* globalObject = jsCast<Zig::GlobalObject*>(init.owner); + auto& vm = init.vm; + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue result = JSValue::decode(Bun__Jest__createTestModuleObject(globalObject)); + JSObject* testModule = result.toObject(globalObject); + RETURN_IF_EXCEPTION(scope, ); + + // Add node:assert as "assert" export for vitest compatibility + JSValue assertModule = + globalObject->internalModuleRegistry()->requireId(globalObject, vm, Bun::InternalModuleRegistry::Field::NodeAssert); + RETURN_IF_EXCEPTION(scope, ); + if (assertModule && !assertModule.isUndefinedOrNull()) { + testModule->putDirect(vm, Identifier::fromString(vm, "assert"_s), assertModule); + RETURN_IF_EXCEPTION(scope, ); + } + + init.set(testModule); });This way a failure to load
node:assertsimply means theassertexport is absent, without leaving a stray pending exception from a lazy initializer.