From 2d5e8d8efa21c546dbe5d000fb7f36cd1ca5c5d0 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sat, 26 Mar 2022 19:21:55 -0500 Subject: [PATCH] feat(testing): Add behavior-driven development --- testing/README.md | 296 ++++ testing/_test_suite.ts | 348 +++++ testing/bdd.ts | 407 ++++++ testing/bdd_examples/user.ts | 22 + testing/bdd_examples/user_flat_test.ts | 43 + testing/bdd_examples/user_mixed_test.ts | 42 + testing/bdd_examples/user_nested_test.ts | 42 + testing/bdd_examples/user_test.ts | 31 + testing/bdd_test.ts | 1623 ++++++++++++++++++++++ 9 files changed, 2854 insertions(+) create mode 100644 testing/_test_suite.ts create mode 100644 testing/bdd.ts create mode 100644 testing/bdd_examples/user.ts create mode 100644 testing/bdd_examples/user_flat_test.ts create mode 100644 testing/bdd_examples/user_mixed_test.ts create mode 100644 testing/bdd_examples/user_nested_test.ts create mode 100644 testing/bdd_examples/user_test.ts create mode 100644 testing/bdd_test.ts diff --git a/testing/README.md b/testing/README.md index 932eb15c1bf6..b7ebbcc6d010 100644 --- a/testing/README.md +++ b/testing/README.md @@ -277,3 +277,299 @@ with the `BenchmarkRunOptions.silent` flag. Clears all registered benchmarks, so calling `runBenchmarks()` after it wont run them. Filtering can be applied by setting `BenchmarkRunOptions.only` and/or `BenchmarkRunOptions.skip` to regular expressions matching benchmark names. + +## Behavior-driven development + +With the `bdd.ts` module you can write your tests in a familiar format for +grouping tests and adding setup/teardown hooks used by other JavaScript testing +frameworks like Jasmine, Jest, and Mocha. + +The `describe` function creates a block that groups together several related +tests. The `it` function registers an individual test case. The `describe` and +`it` functions have similar call signatures to `Deno.test`, making it easy to +migrate from using `Deno.test`. + +### Hooks + +There are 4 types of hooks available for test suites. A test suite can have +multiples of each type of hook, they will be called in the order that they are +registered. The `afterEach` and `afterAll` hooks will be called whether or not +the test case passes. The all hooks will be called once for the whole group +while the each hooks will be called for each individual test case. + +- `beforeAll`: Runs before all of the tests in the test suite. +- `afterAll`: Runs after all of the tests in the test suite finish. +- `beforeEach`: Runs before each of the individual test cases in the test suite. +- `afterEach`: Runs after each of the individual test cases in the test suite. + +If a hook is registered at the top level, a global test suite will be registered +and all tests will belong to it. Hooks registered at the top level must be +registered before any individual test cases or test suites. + +### Focusing tests + +If you would like to only run specific individual test cases, you can do so by +calling `it.only` instead of `it`. If you would like to only run specific test +suites, you can do so by calling `describe.only` instead of `describe`. + +There is one limitation to this when the individual test cases or test suites +belong to another test suite, they will be the only ones to run within the top +level test suite. + +### Ignoring tests + +If you would like to not run specific individual test cases, you can do so by +calling `it.ignore` instead of `it`. If you would like to only run specific test +suites, you can do so by calling `describe.ignore` instead of `describe`. + +### Sanitization options + +Like `Deno.TestDefinition`, the `DescribeDefinition` and `ItDefinition` have +sanitization options. They work in the same way. + +- sanitizeExit: Ensure the test case does not prematurely cause the process to + exit, for example via a call to Deno.exit. Defaults to true. +- sanitizeOps: Check that the number of async completed ops after the test is + the same as number of dispatched ops. Defaults to true. +- sanitizeResources: Ensure the test case does not "leak" resources - ie. the + resource table after the test has exactly the same contents as before the + test. Defaults to true. + +### Permissions option + +Like `Deno.TestDefinition`, the `DescribeDefintion` and `ItDefinition` have a +permissions option. They specify the permissions that should be used to run an +individual test case or test suite. Set this to "inherit" to keep the calling +thread's permissions. Set this to "none" to revoke all permissions. + +Defaults to "inherit". + +There is currently one limitation to this, you cannot use the permissions option +on an individual test case or test suite that belongs to another test suite. + +### Migrating and usage + +To migrate from `Deno.test`, all you have to do is replace `Deno.test` with +`it`. If you are using the step API, you will need to replace `Deno.test` with +describe and steps with `describe` or `it`. The callback for individual test +cases can be syncronous or asyncronous. + +Below is an example of a test file using `Deno.test` and `t.step`. In the +following sections there are examples of how it can be converted to using nested +test grouping, flat test grouping, and a mix of both. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +Deno.test("User.users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +Deno.test("User constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +Deno.test("User age", async (t) => { + const user = new User("Kyle"); + + await t.step("getAge", () => { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + await t.step("setAge", () => { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); +``` + +#### Nested test grouping + +Tests created within the callback of a `describe` function call will belong to +the new test suite it creates. The hooks can be created within it or be added to +the options argument for describe. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + afterEach, + beforeEach, + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + describe("age", () => { + let user: User; + + beforeEach(() => { + user = new User("Kyle"); + }); + + afterEach(() => { + User.users.clear(); + }); + + it("getAge", function () { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it("setAge", function () { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); + }); +}); +``` + +#### Flat test grouping + +The `describe` function returns a unique symbol that can be used to reference +the test suite for adding tests to it without having to create them within a +callback. The gives you the ability to have test grouping without any extra +indentation in front of the grouped tests. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +const userTests = describe("User"); + +it(userTests, "users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +it(userTests, "constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +const ageTests = describe({ + name: "age", + suite: userTests, + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, +}); + +it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); +}); + +it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); +}); +``` + +#### Mixed test grouping + +Both nested test grouping and flat test grouping can be used together. This can +be useful if you'd like to create deep groupings without all the extra +indentation in front of each line. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + const ageTests = describe({ + name: "age", + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, + }); + + it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); +``` diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts new file mode 100644 index 000000000000..5d2d2a1fba92 --- /dev/null +++ b/testing/_test_suite.ts @@ -0,0 +1,348 @@ +/** The options for creating a test suite with the describe function. */ +export interface DescribeDefinition extends Omit { + fn?: () => void; + /** + * The `describe` function returns a `TestSuite` representing the group of tests. + * If `describe` is called within another `describe` calls `fn`, the suite will default to that parent `describe` calls returned `TestSuite`. + * If `describe` is not called within another `describe` calls `fn`, the suite will default to the `TestSuite` representing the global group of tests. + */ + suite?: TestSuite; + /** Run some shared setup before all of the tests in the suite. */ + beforeAll?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared teardown after all of the tests in the suite. */ + afterAll?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared setup before each test in the suite. */ + beforeEach?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared teardown after each test in the suite. */ + afterEach?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; +} + +/** The options for creating an individual test case with the it function. */ +export interface ItDefinition extends Omit { + fn: (this: T) => void | Promise; + /** + * The `describe` function returns a `TestSuite` representing the group of tests. + * If `it` is called within a `describe` calls `fn`, the suite will default to that parent `describe` calls returned `TestSuite`. + * If `it` is not called within a `describe` calls `fn`, the suite will default to the `TestSuite` representing the global group of tests. + */ + suite?: TestSuite; +} + +/** The names of all the different types of hooks. */ +export type HookNames = "beforeAll" | "afterAll" | "beforeEach" | "afterEach"; + +/** Optional test definition keys. */ +const optionalTestDefinitionKeys: (keyof Deno.TestDefinition)[] = [ + "only", + "permissions", + "ignore", + "sanitizeExit", + "sanitizeOps", + "sanitizeResources", +]; + +/** Optional test step definition keys. */ +const optionalTestStepDefinitionKeys: (keyof Deno.TestStepDefinition)[] = [ + "ignore", + "sanitizeExit", + "sanitizeOps", + "sanitizeResources", +]; + +/** + * A group of tests. + */ +export interface TestSuite { + symbol: symbol; +} + +/** + * An internal representation of a group of tests. + */ +export class TestSuiteInternal implements TestSuite { + symbol: symbol; + protected describe: DescribeDefinition; + protected steps: (TestSuiteInternal | ItDefinition)[]; + protected hasOnlyStep: boolean; + + constructor(describe: DescribeDefinition) { + this.describe = describe; + this.steps = []; + this.hasOnlyStep = false; + + const { suite } = describe; + if (suite && !TestSuiteInternal.suites.has(suite.symbol)) { + throw new Error("suite does not represent a registered test suite"); + } + const testSuite = suite + ? TestSuiteInternal.suites.get(suite.symbol) + : TestSuiteInternal.current; + this.symbol = Symbol(); + TestSuiteInternal.suites.set(this.symbol, this); + + const { fn } = describe; + if (fn) { + const temp = TestSuiteInternal.current; + TestSuiteInternal.current = this; + try { + fn(); + } finally { + TestSuiteInternal.current = temp; + } + } + + if (testSuite) { + TestSuiteInternal.addStep(testSuite, this); + } else { + const { + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = describe; + TestSuiteInternal.registerTest({ + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + fn: async (t) => { + if (!TestSuiteInternal.running) TestSuiteInternal.running = true; + const context = {} as T; + const { beforeAll } = this.describe; + if (typeof beforeAll === "function") { + await beforeAll.call(context); + } else if (beforeAll) { + for (const hook of beforeAll) { + await hook.call(context); + } + } + try { + TestSuiteInternal.active.push(this.symbol); + await TestSuiteInternal.run(this, context, t); + } finally { + TestSuiteInternal.active.pop(); + const { afterAll } = this.describe; + if (typeof afterAll === "function") { + await afterAll.call(context); + } else if (afterAll) { + for (const hook of afterAll) { + await hook.call(context); + } + } + } + }, + }); + } + } + + /** If the test cases have begun executing. */ + static running = false; + + /** If a test has been registered yet. Block adding global hooks if a test has been registered. */ + static started = false; + + /** A map of all test suites by symbol. */ + // deno-lint-ignore no-explicit-any + static suites = new Map>(); + + /** The current test suite being registered. */ + // deno-lint-ignore no-explicit-any + static current: TestSuiteInternal | null = null; + + /** The stack of tests that are actively running. */ + static active: symbol[] = []; + + /** This is used internally for testing this module. */ + static reset(): void { + TestSuiteInternal.running = false; + TestSuiteInternal.started = false; + TestSuiteInternal.current = null; + TestSuiteInternal.active = []; + } + + /** This is used internally to register tests. */ + static registerTest(options: Deno.TestDefinition): void { + options = { ...options }; + optionalTestDefinitionKeys.forEach((key) => { + if (typeof options[key] === "undefined") delete options[key]; + }); + Deno.test(options); + } + + /** Updates all steps within top level suite to have ignore set to true if only is not set to true on step. */ + static addingOnlyStep(suite: TestSuiteInternal) { + if (!suite.hasOnlyStep) { + for (let i = 0; i < suite.steps.length; i++) { + const step = suite.steps[i]!; + if (step instanceof TestSuiteInternal) { + if (!(step.hasOnlyStep || step.describe.only)) { + suite.steps.splice(i--, 1); + } + } else { + if (!step.only) { + suite.steps.splice(i--, 1); + } + } + } + suite.hasOnlyStep = true; + } + } + + /** This is used internally to add steps to a test suite. */ + static addStep( + suite: TestSuiteInternal, + step: TestSuiteInternal | ItDefinition, + ): void { + if (!suite.hasOnlyStep) { + if (step instanceof TestSuiteInternal) { + if (step.hasOnlyStep || step.describe.only) { + TestSuiteInternal.addingOnlyStep(suite); + } + } else { + if (step.only) TestSuiteInternal.addingOnlyStep(suite); + } + } + + let omit = false; + if (suite.hasOnlyStep) { + if (step instanceof TestSuiteInternal) { + if (!(step.hasOnlyStep || step.describe.only)) omit = true; + } else { + if (!step.only) omit = true; + } + } + + if (!omit) suite.steps.push(step); + } + + /** This is used internally to add hooks to a test suite. */ + static setHook( + suite: TestSuiteInternal, + name: HookNames, + fn: (this: T) => void | Promise, + ): void { + if (suite.describe[name]) { + if (typeof suite.describe[name] === "function") { + suite.describe[name] = [ + suite.describe[name] as ((this: T) => void | Promise), + ]; + } + (suite.describe[name] as ((this: T) => void | Promise)[]).push(fn); + } else { + suite.describe[name] = fn; + } + } + + /** This is used internally to run all steps for a test suite. */ + static async run( + suite: TestSuiteInternal, + context: T, + t: Deno.TestContext, + ): Promise { + for (const step of suite.steps) { + const { + name, + fn, + ignore, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = step instanceof TestSuiteInternal ? step.describe : step; + + const options: Deno.TestStepDefinition = { + name, + ignore, + sanitizeExit, + sanitizeOps, + sanitizeResources, + fn: async (t) => { + if (permissions) { + throw new Error( + "permissions option not available for nested tests", + ); + } + context = { ...context }; + if (step instanceof TestSuiteInternal) { + const { beforeAll } = step.describe; + if (typeof beforeAll === "function") { + await beforeAll.call(context); + } else if (beforeAll) { + for (const hook of beforeAll) { + await hook.call(context); + } + } + try { + TestSuiteInternal.active.push(step.symbol); + await TestSuiteInternal.run(step, context, t); + } finally { + TestSuiteInternal.active.pop(); + const { afterAll } = step.describe; + if (typeof afterAll === "function") { + await afterAll.call(context); + } else if (afterAll) { + for (const hook of afterAll) { + await hook.call(context); + } + } + } + } else { + await TestSuiteInternal.runTest(fn!, context); + } + }, + }; + optionalTestStepDefinitionKeys.forEach((key) => { + if (typeof options[key] === "undefined") delete options[key]; + }); + await t.step(options); + } + } + + static async runTest( + fn: (this: T) => void | Promise, + context: T, + activeIndex = 0, + ) { + const suite = TestSuiteInternal.active[activeIndex]; + const testSuite = suite && TestSuiteInternal.suites.get(suite); + if (testSuite) { + context = { ...context }; + const { beforeEach } = testSuite.describe; + if (typeof beforeEach === "function") { + await beforeEach.call(context); + } else if (beforeEach) { + for (const hook of beforeEach) { + await hook.call(context); + } + } + try { + await TestSuiteInternal.runTest(fn, context, activeIndex + 1); + } finally { + const { afterEach } = testSuite.describe; + if (typeof afterEach === "function") { + await afterEach.call(context); + } else if (afterEach) { + for (const hook of afterEach) { + await hook.call(context); + } + } + } + } else { + await fn.call(context); + } + } +} diff --git a/testing/bdd.ts b/testing/bdd.ts new file mode 100644 index 000000000000..142a02fd341c --- /dev/null +++ b/testing/bdd.ts @@ -0,0 +1,407 @@ +import { + DescribeDefinition, + HookNames, + ItDefinition, + TestSuite, + TestSuiteInternal, +} from "./_test_suite.ts"; +export type { DescribeDefinition, ItDefinition, TestSuite }; + +/** The arguments for an ItFunction. */ +export type ItArgs = + | [options: ItDefinition] + | [ + name: string, + options: Omit, "name">, + ] + | [ + name: string, + fn: (this: T) => void | Promise, + ] + | [fn: (this: T) => void | Promise] + | [ + name: string, + options: Omit, "fn" | "name">, + fn: (this: T) => void | Promise, + ] + | [ + options: Omit, "fn">, + fn: (this: T) => void | Promise, + ] + | [ + options: Omit, "fn" | "name">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "name" | "suite">, + ] + | [ + suite: TestSuite, + name: string, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "fn" | "name" | "suite">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "suite">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "name" | "suite">, + fn: (this: T) => void | Promise, + ]; + +/** Generates an ItDefinition from ItArgs. */ +function itDefinition(...args: ItArgs): ItDefinition { + let [ + suiteOptionsOrNameOrFn, + optionsOrNameOrFn, + optionsOrFn, + fn, + ] = args; + let suite: TestSuite | undefined = undefined; + let name: string; + let options: + | ItDefinition + | Omit, "fn"> + | Omit, "name"> + | Omit, "fn" | "name">; + if ( + typeof suiteOptionsOrNameOrFn === "object" && + typeof (suiteOptionsOrNameOrFn as TestSuite).symbol === "symbol" + ) { + suite = suiteOptionsOrNameOrFn as TestSuite; + } else { + fn = optionsOrFn as typeof fn; + optionsOrFn = optionsOrNameOrFn as typeof optionsOrFn; + optionsOrNameOrFn = suiteOptionsOrNameOrFn as typeof optionsOrNameOrFn; + } + if (typeof optionsOrNameOrFn === "string") { + name = optionsOrNameOrFn; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + options = {}; + } else { + options = optionsOrFn!; + if (!fn) fn = (options as Omit, "name">).fn; + } + } else if (typeof optionsOrNameOrFn === "function") { + fn = optionsOrNameOrFn; + name = fn.name; + options = {}; + } else { + options = optionsOrNameOrFn!; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + } else { + fn = (options as ItDefinition).fn; + } + name = (options as ItDefinition).name ?? fn.name; + } + + return { + suite, + ...options, + name, + fn, + }; +} + +/** Registers an individual test case. */ +export interface it { + (...args: ItArgs): void; + + /** Registers an individual test case with only set to true. */ + only(...args: ItArgs): void; + + /** Registers an individual test case with ignore set to true. */ + ignore(...args: ItArgs): void; +} + +/** Registers an individual test case. */ +export function it(...args: ItArgs): void { + if (TestSuiteInternal.running) { + throw new Error( + "cannot register new test cases after already registered test cases start running", + ); + } + const options = itDefinition(...args); + const { suite } = options; + const testSuite = suite + ? TestSuiteInternal.suites.get(suite.symbol) + : TestSuiteInternal.current; + + if (!TestSuiteInternal.started) TestSuiteInternal.started = true; + if (testSuite) { + TestSuiteInternal.addStep(testSuite, options); + } else { + const { + name, + fn, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = options; + TestSuiteInternal.registerTest({ + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + async fn() { + if (!TestSuiteInternal.running) TestSuiteInternal.running = true; + await fn.call({} as T); + }, + }); + } +} + +it.only = function itOnly(...args: ItArgs): void { + const options = itDefinition(...args); + return it({ + ...options, + only: true, + }); +}; + +it.ignore = function itIgnore(...args: ItArgs): void { + const options = itDefinition(...args); + return it({ + ...options, + ignore: true, + }); +}; + +function addHook( + name: HookNames, + fn: (this: T) => void | Promise, +): void { + if (!TestSuiteInternal.current) { + if (TestSuiteInternal.started) { + throw new Error( + "cannot add global hooks after a global test is registered", + ); + } + TestSuiteInternal.current = new TestSuiteInternal({ + name: "global", + [name]: fn, + }); + } else { + TestSuiteInternal.setHook(TestSuiteInternal.current!, name, fn); + } +} + +/** Run some shared setup before all of the tests in the suite. */ +export function beforeAll( + fn: (this: T) => void | Promise, +): void { + addHook("beforeAll", fn); +} + +/** Run some shared teardown after all of the tests in the suite. */ +export function afterAll( + fn: (this: T) => void | Promise, +): void { + addHook("afterAll", fn); +} + +/** Run some shared setup before each test in the suite. */ +export function beforeEach( + fn: (this: T) => void | Promise, +): void { + addHook("beforeEach", fn); +} + +/** Run some shared teardown after each test in the suite. */ +export function afterEach( + fn: (this: T) => void | Promise, +): void { + addHook("afterEach", fn); +} + +/** The arguments for a DescribeFunction. */ +export type DescribeArgs = + | [options: DescribeDefinition] + | [name: string] + | [ + name: string, + options: Omit, "name">, + ] + | [name: string, fn: () => void] + | [fn: () => void] + | [ + name: string, + options: Omit, "fn" | "name">, + fn: () => void, + ] + | [ + options: Omit, "fn">, + fn: () => void, + ] + | [ + options: Omit, "fn" | "name">, + fn: () => void, + ] + | [ + suite: TestSuite, + name: string, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "name" | "suite">, + ] + | [ + suite: TestSuite, + name: string, + fn: () => void, + ] + | [ + suite: TestSuite, + fn: () => void, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "fn" | "name" | "suite">, + fn: () => void, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "suite">, + fn: () => void, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "name" | "suite">, + fn: () => void, + ]; + +/** Generates a DescribeDefinition from DescribeArgs. */ +function describeDefinition( + ...args: DescribeArgs +): DescribeDefinition { + let [ + suiteOptionsOrNameOrFn, + optionsOrNameOrFn, + optionsOrFn, + fn, + ] = args; + let suite: TestSuite | undefined = undefined; + let name: string; + let options: + | DescribeDefinition + | Omit, "fn"> + | Omit, "name"> + | Omit, "fn" | "name">; + if ( + typeof suiteOptionsOrNameOrFn === "object" && + typeof (suiteOptionsOrNameOrFn as TestSuite).symbol === "symbol" + ) { + suite = suiteOptionsOrNameOrFn as TestSuite; + } else { + fn = optionsOrFn as typeof fn; + optionsOrFn = optionsOrNameOrFn as typeof optionsOrFn; + optionsOrNameOrFn = suiteOptionsOrNameOrFn as typeof optionsOrNameOrFn; + } + if (typeof optionsOrNameOrFn === "string") { + name = optionsOrNameOrFn; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + options = {}; + } else { + options = optionsOrFn ?? {}; + if (!fn) fn = (options as Omit, "name">).fn; + } + } else if (typeof optionsOrNameOrFn === "function") { + fn = optionsOrNameOrFn; + name = fn.name; + options = {}; + } else { + options = optionsOrNameOrFn ?? {}; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + } else { + fn = (options as DescribeDefinition).fn; + } + name = (options as DescribeDefinition).name ?? fn?.name ?? ""; + } + + if (!suite) { + suite = options.suite; + } + if (!suite && TestSuiteInternal.current) { + const { symbol } = TestSuiteInternal.current; + suite = { symbol }; + } + + return { + ...options, + suite, + name, + fn, + }; +} + +/** Registers a test suite. */ +export interface describe { + (...args: DescribeArgs): TestSuite; + + /** Registers a test suite with only set to true. */ + only(...args: DescribeArgs): TestSuite; + + /** Registers a test suite with ignore set to true. */ + ignore(...args: DescribeArgs): TestSuite; +} + +/** Registers a test suite. */ +export function describe( + ...args: DescribeArgs +): TestSuite { + if (TestSuiteInternal.running) { + throw new Error( + "cannot register new test suites after already registered test cases start running", + ); + } + const options = describeDefinition(...args); + if (!TestSuiteInternal.started) TestSuiteInternal.started = true; + const { symbol } = new TestSuiteInternal(options); + return { symbol }; +} + +describe.only = function describeOnly( + ...args: DescribeArgs +): TestSuite { + const options = describeDefinition(...args); + return describe({ + ...options, + only: true, + }); +}; + +describe.ignore = function describeIgnore( + ...args: DescribeArgs +): TestSuite { + const options = describeDefinition(...args); + return describe({ + ...options, + ignore: true, + }); +}; diff --git a/testing/bdd_examples/user.ts b/testing/bdd_examples/user.ts new file mode 100644 index 000000000000..ae1ce2091a21 --- /dev/null +++ b/testing/bdd_examples/user.ts @@ -0,0 +1,22 @@ +export class User { + static users: Map = new Map(); + age?: number; + + constructor(public name: string) { + if (User.users.has(name)) { + throw new Error(`User ${name} already exists`); + } + User.users.set(name, this); + } + + getAge(): number { + if (!this.age) { + throw new Error("Age unknown"); + } + return this.age; + } + + setAge(age: number) { + this.age = age; + } +} diff --git a/testing/bdd_examples/user_flat_test.ts b/testing/bdd_examples/user_flat_test.ts new file mode 100644 index 000000000000..61bd7faf1770 --- /dev/null +++ b/testing/bdd_examples/user_flat_test.ts @@ -0,0 +1,43 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +const userTests = describe("User"); + +it(userTests, "users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +it(userTests, "constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +const ageTests = describe({ + name: "age", + suite: userTests, + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, +}); + +it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); +}); + +it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); +}); diff --git a/testing/bdd_examples/user_mixed_test.ts b/testing/bdd_examples/user_mixed_test.ts new file mode 100644 index 000000000000..ea2a21df8ac8 --- /dev/null +++ b/testing/bdd_examples/user_mixed_test.ts @@ -0,0 +1,42 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + const ageTests = describe({ + name: "age", + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, + }); + + it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); diff --git a/testing/bdd_examples/user_nested_test.ts b/testing/bdd_examples/user_nested_test.ts new file mode 100644 index 000000000000..4463fe02a5c4 --- /dev/null +++ b/testing/bdd_examples/user_nested_test.ts @@ -0,0 +1,42 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { afterEach, beforeEach, describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + describe("age", () => { + let user: User; + + beforeEach(() => { + user = new User("Kyle"); + }); + + afterEach(() => { + User.users.clear(); + }); + + it("getAge", function () { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it("setAge", function () { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); + }); +}); diff --git a/testing/bdd_examples/user_test.ts b/testing/bdd_examples/user_test.ts new file mode 100644 index 000000000000..dffd31727344 --- /dev/null +++ b/testing/bdd_examples/user_test.ts @@ -0,0 +1,31 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { User } from "./user.ts"; + +Deno.test("User.users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +Deno.test("User constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +Deno.test("User age", async (t) => { + const user = new User("Kyle"); + + await t.step("getAge", () => { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + await t.step("setAge", () => { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts new file mode 100644 index 000000000000..617aae0f2b58 --- /dev/null +++ b/testing/bdd_test.ts @@ -0,0 +1,1623 @@ +import { + assert, + assertEquals, + assertObjectMatch, + assertStrictEquals, +} from "./asserts.ts"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + it, +} from "./bdd.ts"; +import { TestSuiteInternal } from "./_test_suite.ts"; +import { assertSpyCall, assertSpyCalls, Spy, spy, stub } from "./mock/mod.ts"; + +Deno.test("global", async (t) => { + class TestContext implements Deno.TestContext { + steps: TestContext[]; + spies: { + step: Spy; + }; + + constructor() { + this.spies = { + step: spy(this, "step"), + }; + this.steps = []; + } + + async step(t: Deno.TestStepDefinition): Promise; + async step( + name: string, + fn: (t: Deno.TestContext) => void | Promise, + ): Promise; + async step( + tOrName: Deno.TestStepDefinition | string, + fn?: (t: Deno.TestContext) => void | Promise, + ): Promise { + let ignore = false; + if (typeof tOrName === "object") { + ignore = tOrName.ignore ?? false; + fn = tOrName.fn; + } + + const context = new TestContext(); + this.steps.push(context); + if (!ignore) { + await fn!(context); + } + return !ignore; + } + } + + const baseStepOptions: Omit = { + ignore: false, + sanitizeExit: true, + sanitizeOps: true, + sanitizeResources: true, + }; + + const baseOptions: Omit = { + ...baseStepOptions, + only: false, + permissions: "inherit", + }; + + interface GlobalContext { + allTimer: number; + eachTimer: number; + } + + let timerIdx = 1; + const timers = new Map(); + function hookFns() { + timerIdx = 1; + timers.clear(); + return { + beforeAllFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + this.allTimer = timerIdx++; + timers.set(this.allTimer, setTimeout(() => {}, 10000)); + }), + afterAllFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.allTimer)); + }), + beforeEachFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + this.eachTimer = timerIdx++; + timers.set(this.eachTimer, setTimeout(() => {}, 10000)); + }), + afterEachFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.eachTimer)); + }), + }; + } + + await t.step("global hooks", async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "global"); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + }); + + await t.step("it", async (t) => { + async function assertOptions( + expectedOptions: Omit, + cb: (fn: Spy) => void, + ): Promise { + const test = stub(Deno, "test"); + const fn = spy(); + try { + cb(fn); + + assertSpyCalls(fn, 0); + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn", ...Object.keys(expectedOptions)].sort(), + ); + assertObjectMatch(options, { + name: "example", + ...expectedOptions, + }); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 0); + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({}, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions(baseOptions, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 3", async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", fn), undefined); + })); + + await t.step("signature 4", async () => + await assertMinimumOptions((fn) => { + assertEquals( + it(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + + await t.step("only", async (t) => { + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ only: true }, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ...baseOptions, only: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step( + "signature 3", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", fn), undefined); + }), + ); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.only(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.only({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + }); + + await t.step("ignore", async (t) => { + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ignore: true }, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ...baseOptions, ignore: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step( + "signature 3", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", fn), undefined); + }), + ); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.ignore(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.ignore({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + }); + }); + + await t.step("describe", async (t) => { + async function assertOptions( + expectedOptions: Omit, + cb: (fns: Spy[]) => void, + ): Promise { + const test = stub(Deno, "test"); + const fns = [spy(), spy()]; + try { + cb(fns); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn", ...Object.keys(expectedOptions)].sort(), + ); + assertObjectMatch(options, { + name: "example", + ...expectedOptions, + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({}, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("only", async (t) => { + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ only: true }, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions, only: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + }); + + await t.step("ignore", async (t) => { + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ignore: true }, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions, ignore: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + }); + + await t.step("nested only", async (t) => { + async function assertOnly( + cb: (fns: Spy[]) => void, + ): Promise { + const test = stub(Deno, "test"); + const fns = [spy(), spy(), spy()]; + try { + cb(fns); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn"].sort(), + ); + assertObjectMatch(options, { + name: "example", + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + let fn = fns[0]; + assertSpyCalls(fn, 0); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[2]; + assertSpyCalls(fn, 0); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + await t.step("it", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + assertEquals(it.only({ name: "b", fn: fns[1] }), undefined); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("nested it", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe("nested", () => { + assertEquals(it.only({ name: "b", fn: fns[1] }), undefined); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("describe", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe.only("nested", () => { + assertEquals(it({ name: "b", fn: fns[1] }), undefined); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("nested describe", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe("nested", () => { + describe.only("nested 2", () => { + assertEquals(it({ name: "b", fn: fns[1] }), undefined); + }); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + }); + + await t.step("with hooks", async (t) => { + async function assertHooks( + cb: ( + options: { + beforeAllFn: Spy; + afterAllFn: Spy; + beforeEachFn: Spy; + afterEachFn: Spy; + fns: Spy[]; + }, + ) => void, + ) { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + cb({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + } + + await t.step( + "in callback", + async () => + await assertHooks( + ({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }) => { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }, + ), + ); + + await t.step( + "in options", + async () => + await assertHooks( + ({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }) => { + describe({ + name: "example", + beforeAll: beforeAllFn, + afterAll: afterAllFn, + beforeEach: beforeEachFn, + afterEach: afterEachFn, + fn: () => { + assertEquals( + it({ name: "example 1", fn: fns[0] }), + undefined, + ); + assertEquals( + it({ name: "example 2", fn: fns[1] }), + undefined, + ); + }, + }); + }, + ), + ); + + await t.step( + "nested", + async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + describe("nested", () => { + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + let context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + context = context.steps[0]; + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + }, + ); + + interface NestedContext extends GlobalContext { + allTimerNested: number; + eachTimerNested: number; + } + + await t.step( + "nested with hooks", + async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(), + beforeAllFnNested = spy(async function (this: NestedContext) { + await Promise.resolve(); + this.allTimerNested = timerIdx++; + timers.set( + this.allTimerNested, + setTimeout(() => {}, 10000), + ); + }), + afterAllFnNested = spy( + async function (this: NestedContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.allTimerNested)); + }, + ), + beforeEachFnNested = spy(async function (this: NestedContext) { + await Promise.resolve(); + this.eachTimerNested = timerIdx++; + timers.set( + this.eachTimerNested, + setTimeout(() => {}, 10000), + ); + }), + afterEachFnNested = spy( + async function (this: NestedContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.eachTimerNested)); + }, + ); + + try { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + describe("nested", () => { + beforeAll(beforeAllFnNested); + afterAll(afterAllFnNested); + + beforeEach(beforeEachFnNested); + afterEach(afterEachFnNested); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + let context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + context = context.steps[0]; + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { + allTimer: 1, + allTimerNested: 2, + eachTimer: 3, + eachTimerNested: 4, + }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { + allTimer: 1, + allTimerNested: 2, + eachTimer: 5, + eachTimerNested: 6, + }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + + assertSpyCalls(beforeAllFnNested, 1); + assertSpyCalls(afterAllFnNested, 1); + assertSpyCalls(beforeEachFnNested, 2); + assertSpyCalls(afterEachFnNested, 2); + }, + ); + }); + }); +});