Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/test/lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ aroundAll(async runSuite => {

- **Order.** Hooks nest. The first hook you register is the outermost one. The hooks of an outer `describe` block go around the hooks of an inner one.
- **Arguments.** `aroundEach` calls the hook with `(runTest, context, suite)`. `context` is the [test context](/test/writing-tests#test-context). `aroundAll` calls it with `(runSuite, {}, suite)`.
- **Retries.** `aroundEach` runs again for each retry and each repeat. It doesn't run for a skipped or todo test. `aroundAll` doesn't run if every test in its block is skipped.
- **Retries.** `aroundEach` runs again for each retry and each repeat. It doesn't run for a skipped or todo test. `aroundAll` doesn't run if every test in its block is skipped. The `beforeAll` and `afterAll` hooks inside that block then don't run either.
- **Failures.** The test fails if the hook throws, or if it finishes without calling `runTest()`. If the hook fails before it calls `runTest()`, Bun skips everything inside, and the hooks around it still finish. A second `runTest()` call returns a rejected promise.
- **Timeout.** The second argument is a timeout in milliseconds: `aroundEach(fn, 1000)`. It applies to the time before `runTest()` is called, and again to the time after its promise resolves. Bun doesn't count the time in between.

Expand Down
2 changes: 1 addition & 1 deletion docs/test/writing-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ In `bun test`, `"vitest"` is a built-in module, like `"bun:test"` and `"@jest/gl
| `test(name)` without a callback | an error | a todo test |
| Mock functions before each test | left alone | cleared ([`clearMocks`](/test/configuration#test-execution)) |
| Default hook timeout | same as a test's | 10 seconds, or `--timeout` if it's longer |
| `beforeAll` in a scope with only skipped tests | runs | doesn't run |
| `beforeAll` in a scope with only skipped tests | runs, unless its `aroundAll` doesn't | doesn't run |
| Hook registered while a test runs | an error, or runs after that test | never runs |
| `.not.toMatchObject()` with a value that isn't an object | an error | passes |
| `toHaveProperty("a.b")` on `{ "a.b": 1 }` | fails: `"a.b"` is always a path | passes: an own key with that name comes before the path |
Expand Down
1 change: 1 addition & 0 deletions packages/bun-types/test.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,7 @@ declare module "bun:test" {
* async context `runSuite()` is called in. If the hook fails before it calls `runSuite()`, none of them run.
*
* Nesting, failures and the timeout work as in {@link aroundEach}. The hook doesn't run if every test in the block is skipped.
* The `beforeAll` and `afterAll` hooks inside that block then don't run either.
*
* @example
* ```ts
Expand Down
14 changes: 9 additions & 5 deletions src/runtime/test_runner/Order.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ impl Order {
/// Uses an explicit stack instead of recursion, because `describe()` can nest arbitrarily deep.
pub(crate) fn generate_order_describe(&mut self, root: &mut DescribeScope) -> JsResult<()> {
let mut open: Vec<OpenDescribe<'_>> = Vec::new();
if let Some(frame) = self.enter_describe(root)? {
if let Some(frame) = self.enter_describe(root, true)? {
open.push(frame);
}
while let Some(frame) = open.last_mut() {
Expand All @@ -97,7 +97,7 @@ impl Order {
}
match entry {
TestScheduleEntry::Describe(describe) => {
if let Some(child) = self.enter_describe(describe)? {
if let Some(child) = self.enter_describe(describe, frame.use_hooks)? {
open.push(child);
}
}
Expand All @@ -109,18 +109,22 @@ impl Order {
Ok(())
}

/// `parent_use_hooks` is the `use_hooks` of the enclosing scope, or `true` for a file's root scope.
fn enter_describe<'a>(
&mut self,
current: &'a mut DescribeScope,
parent_use_hooks: bool,
) -> JsResult<Option<OpenDescribe<'a>>> {
if current.failed {
return Ok(None); // do not schedule any tests in a failed describe scope
}
let use_hooks = self.cfg.always_use_hooks || current.base.has_callback;
// A scope that runs no test leaves out its `aroundAll`, and with it the hooks that `aroundAll` wraps, nested scopes included.
let use_hooks = parent_use_hooks
&& (current.base.has_callback || (self.cfg.always_use_hooks && current.around_all.is_empty()));

// gather aroundAll
let aroundall_order: AllOrderResult = if use_hooks {
self.generate_all_order(&current.around_all, current.base.has_callback)?
self.generate_all_order(&current.around_all, true)?
} else {
AllOrderResult::EMPTY
};
Expand Down Expand Up @@ -174,7 +178,7 @@ impl Order {
// An aroundAll that fails before `runSuite()` skips to the teardown of the ones around it. Its own slot has no callback.
frame.aroundall_order.set_failure_skip_to(self);
if frame.use_hooks {
self.generate_all_order(frame.around_all_teardown, frame.has_callback)?;
self.generate_all_order(frame.around_all_teardown, true)?;
}

Ok(())
Expand Down
198 changes: 198 additions & 0 deletions test/js/bun/test/test-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,204 @@ describe.concurrent("hooks of a describe block that does not run", () => {
});
});

// "bun:test" runs beforeAll and afterAll in a block whose tests are all skipped, but never outside the aroundAll that wraps them.
describe.concurrent("where no test runs, an aroundAll stays out and so do the hooks inside it", () => {
async function run(files: Record<string, string>, ...args: string[]) {
using dir = tempDir("hooks-inside-around-all", files);
await using proc = spawn({
cmd: [bunExe(), "test", ...args],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return {
log: stdout.split("\n").filter(line => line && !line.startsWith("bun test ")),
results: stderr
.split("\n")
.filter(line => /^\((pass|fail|skip|todo)\) /.test(line))
.map(line => line.replace(/ \[[\d.]+ms\]$/, "")),
errors: stderr.split("\n").filter(line => /^\w*[eE]rror: /.test(line)),
exitCode,
};
}

const importAll = (module: string) => `import { test, describe, beforeAll, afterAll, aroundAll } from "${module}";`;
const blocks = `
import { AsyncLocalStorage } from "node:async_hooks";
const store = new AsyncLocalStorage();
const wrap = name => aroundAll(async runSuite => {
console.log(name, "aroundAll >");
await store.run(name, runSuite);
console.log(name, "aroundAll <");
});
const hooks = name => {
beforeAll(() => console.log(name, "beforeAll inside", store.getStore()));
afterAll(() => console.log(name, "afterAll inside", store.getStore()));
};
describe("runs a test", () => {
wrap("runs a test");
hooks("runs a test");
test("test", () => {});
describe("skipped, with its own aroundAll", () => {
wrap("unreachable");
hooks("unreachable");
test.skip("test", () => {});
});
describe("skipped, without one", () => {
hooks("skipped, without one");
test.skip("test", () => {});
});
});
describe("skipped", () => {
wrap("unreachable");
hooks("unreachable");
test.skip("test", () => {});
describe("nested", () => {
hooks("unreachable");
describe("twice", () => {
hooks("unreachable");
test.skip("test", () => {});
});
});
});
describe("afterAll only", () => {
wrap("unreachable");
afterAll(() => console.log("unreachable"));
test.skip("test", () => {});
});
describe("todo", () => {
wrap("unreachable");
hooks("unreachable");
test.todo("test");
});
describe("skipIf", () => {
wrap("unreachable");
hooks("unreachable");
test.skipIf(true)("test", () => {});
});
describe("no tests", () => {
wrap("unreachable");
hooks("unreachable");
});
describe("skipped, and nothing wraps it", () => {
hooks("nothing wraps it");
test.skip("test", () => {});
});
`;
const insideTheOneThatRuns = (skippedBlock: string[]) => [
"runs a test aroundAll >",
"runs a test beforeAll inside runs a test",
...skippedBlock,
"runs a test afterAll inside runs a test",
"runs a test aroundAll <",
];
const ofBun = [
...insideTheOneThatRuns([
"skipped, without one beforeAll inside runs a test",
"skipped, without one afterAll inside runs a test",
]),
"nothing wraps it beforeAll inside undefined",
"nothing wraps it afterAll inside undefined",
];
test.each([
[`"bun:test"`, importAll("bun:test"), ofBun],
[`"@jest/globals"`, importAll("@jest/globals"), ofBun],
["the globals", "", ofBun],
[
`"bun:test", inside an aroundAll from "vitest"`,
`import { test, describe, beforeAll, afterAll } from "bun:test"; import { aroundAll } from "vitest";`,
ofBun,
],
[`"vitest"`, importAll("vitest"), insideTheOneThatRuns([])],
])("hooks from %s", async (_, header, expected) => {
const { log, results, errors, exitCode } = await run({ "a.test.js": header + blocks });
expect({ log, results, errors, exitCode }).toEqual({
log: expected,
results: [
"(pass) runs a test > test",
"(skip) runs a test > skipped, with its own aroundAll > test",
"(skip) runs a test > skipped, without one > test",
"(skip) skipped > test",
"(skip) skipped > nested > twice > test",
"(skip) afterAll only > test",
"(todo) todo > test",
"(skip) skipIf > test",
"(skip) skipped, and nothing wraps it > test",
],
errors: [],
exitCode: 0,
});
});

test("a beforeAll doesn't run without the set-up of its aroundAll", async () => {
const { results, errors, exitCode } = await run({
"a.test.js": `
${importAll("bun:test")}
let db;
describe("D", () => {
aroundAll(async runSuite => { db = { query: () => 1 }; await runSuite(); db = undefined; });
beforeAll(() => { db.query(); });
test.skip("S", () => {});
});
test("T", () => {});
`,
});
expect({ results, errors, exitCode }).toEqual({ results: ["(skip) D > S", "(pass) T"], errors: [], exitCode: 0 });
});

test("a file's aroundAll keeps the file's hooks out, and the hooks of a scope above an aroundAll still run", async () => {
const { log, results, errors, exitCode } = await run(
{
"preload.js": `
${importAll("bun:test")}
aroundAll(async runSuite => { console.log("preload aroundAll >"); await runSuite(); console.log("preload aroundAll <"); });
beforeAll(() => console.log("preload beforeAll"));
afterAll(() => console.log("preload afterAll"));
`,
"a.test.js": `
${importAll("bun:test")}
aroundAll(() => console.log("unreachable"));
beforeAll(() => console.log("unreachable"));
afterAll(() => console.log("unreachable"));
describe("block", () => {
beforeAll(() => console.log("unreachable"));
afterAll(() => console.log("unreachable"));
test.skip("in a", () => {});
});
`,
"b.test.js": `
${importAll("bun:test")}
beforeAll(() => console.log("b beforeAll"));
afterAll(() => console.log("b afterAll"));
describe("block", () => {
aroundAll(() => console.log("unreachable"));
beforeAll(() => console.log("unreachable"));
afterAll(() => console.log("unreachable"));
test.skip("in b", () => {});
});
`,
},
"--preload",
"./preload.js",
);
expect({ log, results, errors, exitCode }).toEqual({
log: [
"preload aroundAll >",
"preload beforeAll",
"b beforeAll",
"b afterAll",
"preload afterAll",
"preload aroundAll <",
],
results: ["(skip) block > in a", "(skip) block > in b"],
errors: [],
exitCode: 0,
});
});
});

it("should run beforeAll() & afterAll() even without tests", async () => {
const test_dir = tmpdirSync();
try {
Expand Down
Loading