+# Rhum
----
+[![Latest Release](https://img.shields.io/github/release/drashland/rhum.svg?color=bright_green&label=latest)](#)
+[![CI master](https://img.shields.io/github/workflow/status/drashland/rhum/master?label=ci%20-%20master)](#)
+[![YouTube](https://img.shields.io/badge/tutorials-youtube-red)](https://rb.gy/vxmeed)
-### Features
+
-- Descriptive naming for your tests
-- Lightweight
-- Zero 3rd party dependencies
-- Simple and easy to use
-- Asynchronous support
-- Still uses `Deno.test` under the hood
-- Skip functionality
-- Mock requests
-- Hooks
+Rhum is a test double library that follows
+[test double definitions](https://martinfowler.com/bliki/TestDouble.html) from
+Gerard Meszaros.
-### Getting Started
+View the full documentation at https://drash.land/rhum.
-To add Rhum to your project, follow the quick start guide
-[here](https://drash.land/rhum/#/#quickstart).
-
-### Why Use Rhum?
-
-Rhum allows you to write tests in a very descriptive way -- from a code
-perspective or output perspective.
-
-Rhum is designed to aid your testing efforts -- providing many utilities as
-wrappers around Deno's existing `Deno.test`. Rhum is meant to improve the user
-experience when it comes to writing tests, such as:
-
-- Readability for test cases
-- Features that aren't available in Deno yet (hooks)
-
-Rhum takes concepts from the following:
-
-- Mocha — For how you
- write tests in Rhum, and the use of
- hooks
-- Baretest —
- Being minimalistic
-
-Rhum can be added directly into any project. All you need to do is import Rhum
-and you are ready to start writing tests or bring your existing tests under
-Rhum.
-
----
-
-Want to contribute? Follow the Contributing Guidelines
-[here](https://github.com/drashland/.github/blob/master/CONTRIBUTING.md). All
-code is released under the [MIT License](./LICENSE).
+In the event the documentation pages are not accessible, please view the raw
+version of the documentation at
+https://github.com/drashland/website-v2/tree/main/docs.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 00000000..721e8b82
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1 @@
+module.exports = { presets: ["@babel/preset-env"] };
diff --git a/console/build_esm_lib b/console/build_esm_lib
new file mode 100755
index 00000000..82a53f24
--- /dev/null
+++ b/console/build_esm_lib
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+(
+ rm -r tmp/conversion_workspace/
+ mkdir -p tmp/conversion_workspace/
+ cp -r src/ tmp/conversion_workspace/src/
+ cp mod.ts tmp/conversion_workspace/mod.ts
+ deno run --allow-read --allow-run --allow-write ./console/build_esm_lib.ts
+)
diff --git a/console/build_esm_lib.ts b/console/build_esm_lib.ts
new file mode 100644
index 00000000..6a683a74
--- /dev/null
+++ b/console/build_esm_lib.ts
@@ -0,0 +1,48 @@
+const decoder = new TextDecoder();
+const encoder = new TextEncoder();
+
+const filesToRewrite = [
+ "tmp/conversion_workspace/src/fake/fake_builder.ts",
+ "tmp/conversion_workspace/src/fake/fake_mixin.ts",
+ "tmp/conversion_workspace/src/mock/mock_builder.ts",
+ "tmp/conversion_workspace/src/mock/mock_mixin.ts",
+ "tmp/conversion_workspace/src/interfaces.ts",
+ "tmp/conversion_workspace/src/pre_programmed_method.ts",
+ "tmp/conversion_workspace/src/types.ts",
+ "tmp/conversion_workspace/mod.ts",
+];
+
+for (const index in filesToRewrite) {
+ const file = filesToRewrite[index];
+
+ // Step 1: Read contents
+ let contents = decoder.decode(Deno.readFileSync(file));
+
+ // Step 2: Create an array of import/export statements from the contents
+ const importStatements = contents.match(/import.*";/g);
+ const exportStatements = contents.match(/export.*";/g);
+
+ // Step 3: Remove all .ts extensions from the import/export statements
+ const newImportStatements = importStatements?.map((statement: string) => {
+ return statement.replace(/\.ts";/, `";`);
+ });
+
+ const newExportStatements = exportStatements?.map((statement: string) => {
+ return statement.replace(/\.ts";/, `";`);
+ });
+
+ // Step 4: Replace the original contents with the new contents
+ if (newImportStatements) {
+ importStatements?.forEach((statement: string, index: number) => {
+ contents = contents.replace(statement, newImportStatements[index]);
+ });
+ }
+ if (newExportStatements) {
+ exportStatements?.forEach((statement: string, index: number) => {
+ contents = contents.replace(statement, newExportStatements[index]);
+ });
+ }
+
+ // Step 5: Rewrite the original file without .ts extensions
+ Deno.writeFileSync(file, encoder.encode(contents));
+}
diff --git a/deps.ts b/deps.ts
deleted file mode 100644
index fa6c500d..00000000
--- a/deps.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export { BufReader } from "https://deno.land/std@0.135.0/io/bufio.ts";
-
-export * as StdAsserts from "https://deno.land/std@0.135.0/testing/asserts.ts";
-
-export * as colors from "https://deno.land/std@0.135.0/fmt/colors.ts";
diff --git a/egg.json b/egg.json
index 06fa2b92..ca372055 100644
--- a/egg.json
+++ b/egg.json
@@ -1,13 +1,11 @@
{
"name": "rhum",
- "description": "A lightweight testing framework for Deno.",
+ "description": "A test double module to stub and mock your code.",
"version": "1.1.14",
"stable": true,
"repository": "https://github.com/drashland/rhum",
"files": [
"./mod.ts",
- "./deps.ts",
- "./src/**/*",
"./README.md",
"./logo.svg",
"LICENSE"
diff --git a/example_tests/basic/1.ts b/example_tests/basic/1.ts
deleted file mode 100644
index eaae3d3e..00000000
--- a/example_tests/basic/1.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Rhum } from "../../mod.ts";
-
-Rhum.testPlan("test_plan_1", () => {
- Rhum.testSuite("test_suite_1a", () => {
- Rhum.testCase("test_case_1a1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1a2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1a3", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_1b", () => {
- Rhum.testCase("test_case_1b1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1b2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1b3", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-});
-
-Rhum.run();
diff --git a/example_tests/basic/1_fail.ts b/example_tests/basic/1_fail.ts
deleted file mode 100644
index 5924602d..00000000
--- a/example_tests/basic/1_fail.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Rhum } from "../../mod.ts";
-
-Rhum.testPlan("test_plan_1", () => {
- Rhum.testSuite("test_suite_1a", () => {
- Rhum.testCase("test_case_1a1", () => {
- Rhum.asserts.assertEquals(true, false);
- });
- Rhum.testCase("test_case_1a2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1a3", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_1b", () => {
- Rhum.testCase("test_case_1b1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1b2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_1b3", () => {
- Rhum.asserts.assertEquals(true, false);
- });
- });
-});
-
-Rhum.run();
diff --git a/example_tests/basic/2.ts b/example_tests/basic/2.ts
deleted file mode 100644
index c45c87fc..00000000
--- a/example_tests/basic/2.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Rhum } from "../../mod.ts";
-
-Rhum.testPlan("test_plan_2", () => {
- Rhum.testSuite("test_suite_2a", () => {
- Rhum.testCase("test_case_2a1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2a2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2a3", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_2b", () => {
- Rhum.testCase("test_case_2b1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2b2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_2c", () => {
- Rhum.testCase("test_case_2c1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2c2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2c3", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_2d", () => {
- Rhum.testCase("test_case_2d1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_2d2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-});
-
-Rhum.run();
diff --git a/example_tests/basic/3.ts b/example_tests/basic/3.ts
deleted file mode 100644
index 40b52b0c..00000000
--- a/example_tests/basic/3.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Rhum } from "../../mod.ts";
-
-Rhum.testPlan("test_plan_3", () => {
- Rhum.testSuite("test_suite_3a", () => {
- Rhum.testCase("test_case_3a1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_3a2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_3b", () => {
- Rhum.testCase("test_case_3b1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_3b2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-
- Rhum.testSuite("test_suite_3c", () => {
- Rhum.testCase("test_case_3c1", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- Rhum.testCase("test_case_3c2", () => {
- Rhum.asserts.assertEquals(true, true);
- });
- });
-});
-
-Rhum.run();
diff --git a/example_tests/basic/tests_fail.ts b/example_tests/basic/tests_fail.ts
deleted file mode 100644
index 978ca4eb..00000000
--- a/example_tests/basic/tests_fail.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import "./1_fail.ts";
-import "./2.ts";
-import "./3.ts";
diff --git a/example_tests/basic/tests_pass.ts b/example_tests/basic/tests_pass.ts
deleted file mode 100644
index a48c96b7..00000000
--- a/example_tests/basic/tests_pass.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import "./1.ts";
-import "./2.ts";
-import "./3.ts";
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 00000000..4b1f85bb
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,14 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+import type { Config } from "@jest/types";
+
+const config: Config.InitialOptions = {
+ preset: "ts-jest",
+ testEnvironment: "node",
+ verbose: true,
+ moduleFileExtensions: ["ts", "js"],
+ transform: {
+ "^.+\\.(ts|tsx)?$": "ts-jest",
+ "^.+\\.(js|jsx)$": "babel-jest",
+ },
+};
+export default config;
diff --git a/mod.ts b/mod.ts
index 8a58e201..53715a7d 100644
--- a/mod.ts
+++ b/mod.ts
@@ -1,453 +1,124 @@
-import { assertions, asserts } from "./src/rhum_asserts.ts";
-import { TestCase } from "./src/test_case.ts";
-import type { ITestPlan } from "./src/interfaces.ts";
-import type { Constructor, Stubbed } from "./src/types.ts";
-import { MockBuilder } from "./src/mock_builder.ts";
-
-export type { Constructor, Stubbed } from "./src/types.ts";
-export { MockBuilder } from "./src/mock_builder.ts";
+import type { Constructor, StubReturnValue } from "./src/types.ts";
+import { MockBuilder } from "./src/mock/mock_builder.ts";
+import { FakeBuilder } from "./src/fake/fake_builder.ts";
+export * as Types from "./src/types.ts";
+export * as Interfaces from "./src/interfaces.ts";
/**
- * Deno's test runner outputs "test ", which has a length of 5. This module
- * erases the "test " string by backspacing the test plan line and test suite
- * line by that number. For safety, it substracts twice that number. This is
- * how we get the number 10 here.
+ * Create a dummy.
+ *
+ * Per Martin Fowler (based on Gerard Meszaros), "Dummy objects are passed
+ * around but never actually used. Usually they are just used to fill parameter
+ * lists."
+ *
+ * @param constructorFn - The constructor function to use to become the
+ * prototype of the dummy. Dummy objects should be the same instance as what
+ * they are standing in for. For example, if a `SomeClass` parameter needs to be
+ * filled with a dummy because it is out of scope for a test, then the dummy
+ * should be an instance of `SomeClass`.
+ * @returns A dummy object being an instance of the given constructor function.
*/
-const extraChars = 10;
+export function Dummy(constructorFn?: Constructor): T {
+ const dummy = Object.create({});
+ Object.setPrototypeOf(dummy, constructorFn ?? Object);
+ return dummy;
+}
/**
- * This testing framework allows the following syntax:
+ * Get the builder to create fake objects.
*
- * import { Rhum } from "/path/to/rhum/mod.ts";
+ * Per Martin Fowler (based on Gerard Meszaros), "Fake objects actually have
+ * working implementations, but usually take some shortcut which makes them not
+ * suitable for production (an InMemoryTestDatabase is a good example)."
*
- * Rhum.testPlan("test_plan_1", () => {
+ * @param constructorFn - The constructor function of the object to fake.
+ *
+ * @returns Instance of `FakeBuilder`.
+ */
+export function Fake(constructorFn: Constructor): FakeBuilder {
+ return new FakeBuilder(constructorFn);
+}
+
+/**
+ * Get the builder to create mocked objects.
*
- * Rhum.testSuite("test_suite_1a", () => {
- * Rhum.testCase("test_case_1a1", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * Rhum.testCase("test_case_1a2", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * Rhum.testCase("test_case_1a3", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * });
+ * Per Martin Fowler (based on Gerard Meszaros), "Mocks are pre-programmed with
+ * expectations which form a specification of the calls they are expected to
+ * receive. They can throw an exception if they receive a call they don't expect
+ * and are checked during verification to ensure they got all the calls they
+ * were expecting."
*
- * Rhum.testSuite("test_suite_1b", () => {
- * Rhum.testCase("test_case_1b1", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * Rhum.testCase("test_case_1b2", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * Rhum.testCase("test_case_1b3", () => {
- * Rhum.asserts.assertEquals(true, true);
- * });
- * });
+ * @param constructorFn - The constructor function of the object to mock.
*
- * });
+ * @returns Instance of `MockBuilder`.
*/
-export class RhumRunner {
- /**
- * The asserts module from https://deno.land/std/testing, but attached to Rhum
- * for accessibility.
- *
- * Rhum.asserts.assertEquals(true, true); // pass
- * Rhum.asserts.assertEquals(true, false); // fail
- */
- // deno-lint-ignore ban-types Reason for this is, deno lint no longer allows `Function` and instead needs us to be explicit: `() => void`, but because we couldn't use that to type the properties (we would just be copying Deno's interfaces word for word), we have to deal with `Function
- public asserts: { [key in assertions]: Function } = asserts;
-
- protected passed_in_test_plan = "";
-
- protected passed_in_test_suite = "";
-
- protected test_plan_in_progress = "";
-
- protected test_suite_in_progress = "";
-
- protected plan: ITestPlan = { suites: {} };
-
- // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
-
- /**
- * Used to define a hook that will execute before each test suite or test
- * case. If this is used inside of a test plan, then it will execute before
- * each test suite. If this is used inside of a test suite, then it will
- * execute before each test case.
- *
- * @param cb - The callback to invoke. Would contain the required logic you
- * need to do what you want, before each test suite or case.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.beforeEach(() => {
- * // Runs before each test suite in this test plan
- * });
- * Rhum.testSuite("My Suite 1", () => {
- * Rhum.beforeEach(() => {
- * // Runs before each test case in this test suite
- * });
- * Rhum.testCase("My Test Case 1", () => {
- * ...
- * });
- * });
- * });
- */
- public beforeEach(cb: () => void): void {
- // Check if the hook is for test cases inside of a suite
- if (this.passed_in_test_plan && this.passed_in_test_suite) {
- // is a before each inside a suite for every test case
- this.plan.suites![this.passed_in_test_suite].before_each_case_hook = cb;
- } else if (this.passed_in_test_plan && !this.passed_in_test_suite) {
- // before each hooks for the suites
- this.plan.before_each_suite_hook = cb;
- }
- }
-
- /**
- * Used to define a hook that will execute after each test suite or test case.
- * If this is used inside of a test plan, then it will execute after each test
- * suite. If this is used inside of a test suite, then it will execute after
- * each test case.
- *
- * @param cb - The callback to invoke. Would contain the required logic you
- * need to do what you want, after each test suite or case.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.afterEach(() => {
- * // Runs after each test suite in this test plan
- * });
- * Rhum.testSuite("My Suite 1", () => {
- * Rhum.afterEach(() => {
- * // Runs after each test case in this test suite
- * });
- * Rhum.testCase("My Test Case 1", () => {
- * ...
- * });
- * });
- * });
- */
- public afterEach(cb: () => void): void {
- // Check if the hook is for test cases inside of a suite
- if (this.passed_in_test_plan && this.passed_in_test_suite) {
- // is a after each inside a suite for every test case
- this.plan.suites![this.passed_in_test_suite].after_each_case_hook = cb;
- } else if (this.passed_in_test_plan && !this.passed_in_test_suite) {
- // after each hooks for the suites
- this.plan.after_each_suite_hook = cb;
- }
- }
-
- /**
- * Used to define a hook that will execute after all test suites or test
- * cases. If this is used inside of a test plan, then it will execute after
- * all test suites. If this is used inside of a test suite, then it will
- * execute after all test cases.
- *
- * @param cb - The callback to invoke. Would contain the required logic you
- * need to do what you want, after all test suites or cases.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.afterAll(() => {
- * // Runs once after all test suites in this test plan
- * });
- * Rhum.testSuite("My Suite 1", () => {
- * Rhum.afterAll(() => {
- * // Runs once after all test cases in this test suite
- * });
- * Rhum.testCase("My Test Case 1", () => {
- * ...
- * });
- * });
- * });
- */
- public afterAll(cb: () => void): void {
- // Check if the hook is for test cases inside of a suite
- if (this.passed_in_test_plan && this.passed_in_test_suite) {
- // is a before all inside a suite for every test case
- this.plan.suites![this.passed_in_test_suite].after_all_case_hook = cb;
- } else if (this.passed_in_test_plan && !this.passed_in_test_suite) {
- // before all hooks for the suites
- this.plan.after_all_suite_hook = cb;
- }
- }
-
- /**
- * Used to define a hook that will execute before all test suites or test
- * cases. If this is used inside of a test plan, then it will execute before
- * all test suites. If this is used inside of a test suite, then it will
- * execute before all test cases.
- *
- * @param cb - The callback to invoke. Would contain the required logic you
- * need to do what you want, before all test suites or cases.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.beforeAll(() => {
- * // Runs once before all test suites in this test plan
- * });
- * Rhum.testSuite("My Suite 1", () => {
- * Rhum.beforeAll(() => {
- * // Runs once before all test cases in this test suite
- * });
- * Rhum.testCase("My Test Case 1", () => {
- * ...
- * });
- * });
- * });
- */
- public beforeAll(cb: () => void): void {
- // Check if the hook is for test cases inside of a suite
- if (this.passed_in_test_plan && this.passed_in_test_suite) {
- // is a before all inside a suite for every test case
- this.plan.suites![this.passed_in_test_suite].before_all_case_hook = cb;
- } else if (this.passed_in_test_plan && !this.passed_in_test_suite) {
- // before all hooks for the suites
- this.plan.before_all_suite_hook = cb;
- }
- }
-
- // public only(cb: Function): void {
- // // Do something
- // }
-
- /**
- * Allows a test plan, suite, or case to be skipped when the tests run.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.skip("My Suite 1", () => { // will not run this block
- * Rhum.testCase("My Test Case In Suite 1", () => {
- * ...
- * });
- * });
- * Rhum.testSuite("My Suite 2", () => {
- * Rhum.testCase("My Test Case In Suite 2", () => {
- * ...
- * });
- * Rhum.skip("My Other Test Case In Suite 2", () => { // will not run this block
- * ...
- * });
- * });
- * });
- */
- public skip(name: string, cb: () => void): void {
- // TODO(ebebbington|crookse) Maybe we could still call run, but pass in {
- // ignore: true } which the Deno.Test will use? just so it displays ignored
- // in the console
- }
+export function Mock(constructorFn: Constructor): MockBuilder {
+ return new MockBuilder(constructorFn);
+}
- /**
- * Stub a member of an object.
- *
- * @param obj -The object containing the member to stub.
- * @param member -The member to stub.
- * @param value - The return value of the stubbed member.
- *
- * Returns the object in question as a Stubbed type. Being a Stubbed type
- * means it has access to a `.stub()` method for stubbing properties and
- * methods.
- *
- * class MyObject {
- * public some_property = "someValue";
- * }
- *
- * // Define the object that will have stubbed members as a stubbed object
- * const myStubbedObject = Rhum.stubbed(new MyObject());
- *
- * // Stub the object's some_property property to a certain value
- * myStubbedObject.stub("some_property", "this property is now stubbed");
- *
- * // Assert that the property was stubbed
- * Rhum.asserts.assertEquals(myStubbedObject.some_property, "this property is now stubbed");
- */
- public stubbed(obj: T): Stubbed {
- (obj as unknown as { [key: string]: boolean }).is_stubbed = true;
- (obj as unknown as {
- [key: string]: (property: string, value: unknown) => void;
- }).stub = function (
- property: string,
- value: unknown,
- ): void {
- Object.defineProperty(obj, property, {
- value: value,
- });
+/**
+ * Create a stub function that returns "stubbed".
+ */
+export function Stub(): () => "stubbed";
+/**
+ * Take the given object and stub its given data member to return the given
+ * return value.
+ *
+ * @param obj - The object receiving the stub.
+ * @param dataMember - The data member on the object to be stubbed.
+ * @param returnValue - (optional) What the stub should return. Defaults to
+ * "stubbed".
+ */
+export function Stub(
+ obj: T,
+ dataMember: keyof T,
+ returnValue?: R,
+): StubReturnValue;
+/**
+ * Take the given object and stub its given data member to return the given
+ * return value.
+ *
+ * Per Martin Fowler (based on Gerard Meszaros), "Stubs provide canned answers
+ * to calls made during the test, usually not responding at all to anything
+ * outside what's programmed in for the test."
+ *
+ * @param obj - (optional) The object receiving the stub. Defaults to a stub
+ * function.
+ * @param dataMember - (optional) The data member on the object to be stubbed.
+ * Only used if `obj` is an object.
+ * @param returnValue - (optional) What the stub should return. Defaults to
+ * "stubbed" for class properties and a function that returns "stubbed" for
+ * class methods. Only used if `object` is an object and `dataMember` is a
+ * member of that object.
+ */
+export function Stub(
+ obj?: T,
+ dataMember?: keyof T,
+ returnValue?: R,
+): unknown {
+ if (obj === undefined) {
+ return function stubbed() {
+ return "stubbed";
};
-
- return obj as Stubbed;
- }
-
- /**
- * Get the mock builder to mock classes.
- *
- * @param constructorFn - The constructor function of the object to mock.
- *
- * Returns an instance of the MockBuilder class.
- *
- * class ToBeMocked { ... }
- *
- * const mock = Rhum
- * .mock(ToBeMocked)
- * .withConstructorArgs("someArg") // if the class to be mocked has a constructor and it requires args
- * .create();
- */
- public mock(constructorFn: Constructor): MockBuilder {
- return new MockBuilder(constructorFn);
- }
-
- /**
- * A test case is grouped by a test suite and it is what makes the assertions
- * - it is the test. You can define multiple test cases under a test suite.
- * Test cases can also be asynchronous. Test cases can only be defined inside
- * of a test suite.
- *
- * @param name - The name of the test case.
- * @param testFn - The test to execute.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.testSuite("My Suite 1", () => {
- * Rhum.testCase("My Test Case 1", () => {
- * Rhum.assert.assertEquals(something, true);
- * });
- * Rhum.testCase("My Test Case 2", () => {
- * Rhum.assert.assertEquals(something, false);
- * });
- * });
- * });
- */
- public testCase(name: string, testFn: () => void): void {
- this.plan.suites[this.passed_in_test_suite].cases!.push({
- name,
- new_name: this.formatTestCaseName(name),
- testFn,
- });
- }
-
- /**
- * Groups up test suites to describe a test plan. Usually, a test plan is per
- * file and contains the tests suites and test cases for a single file. Test
- * plans are required in order to define a test suite with test cases.
- *
- * @param name - The name of the test plan.
- * @param testSuites - The test suites to execute.
- *
- * Rhum.testPlan("My Plan", () => {
- * ...
- * });
- */
- public testPlan(name: string, testSuites: () => void): void {
- this.passed_in_test_suite = ""; // New plan
- this.passed_in_test_plan = name;
- testSuites();
}
- /**
- * A test suite usually describes a method or property name and groups up all
- * test cases for that method or property. You can define multiple test suites
- * under a test plan. Test suites can only be defined inside of a test plan.
- *
- * @param name - The name of the test suite.
- * @param testCases - The test cases to execute.
- *
- * Rhum.testPlan("My Plan", () => {
- * Rhum.testSuite("My Suite 1", () => {
- * ...
- * });
- * Rhum.testSuite("My Suite 2", () => {
- * ...
- * });
- * });
- */
- public testSuite(name: string, testCases: () => void): void {
- this.passed_in_test_suite = name;
- this.plan.suites![name] = { cases: [] };
- testCases();
- }
-
- /**
- * Run the test plan.
- *
- * Rhum.testPlan("My Plan", () => {
- * ...
- * });
- *
- * Rhum.run();
- */
- public run(): void {
- const tc = new TestCase(this.plan);
- tc.run();
- this.deconstruct();
- }
-
- //////////////////////////////////////////////////////////////////////////////
- // FILE MARKER - METHODS - PROTECTED /////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////
-
- /**
- * Figure out the name of the test case for output purposes.
- *
- * @param name - The name of the test case.
- *
- * Returns the new test name for outputting purposes.
- */
- protected formatTestCaseName(name: string): string {
- let newName: string;
- // (ebebbington) Unfortunately, due to the CI not correctly displaying output
- // (it is all over the place and just completely unreadable as
- // it doesn't play well with our control characters), we need to
- // display the test output differently, based on if the tests are
- // being ran inside a CI or not. Nothing will change for the current
- // way of doing things, but if the tests are being ran inside a CI,
- // the format would be:
- // test | | ... ok (2ms)
- // test | | ... ok (2ms)
- // Even if plans and/or suites are the same. I believe this the best
- // way we can display the output
- if (Deno.env.get("CI") === "true") {
- newName =
- `${this.passed_in_test_plan} | ${this.passed_in_test_suite} | ${name}`;
- return newName;
- }
-
- if (this.test_plan_in_progress != this.passed_in_test_plan) {
- this.test_plan_in_progress = this.passed_in_test_plan;
- this.test_suite_in_progress = this.passed_in_test_suite;
- newName =
- `${
- "\u0008".repeat(name.length + extraChars)
- } ${this.passed_in_test_suite}` +
- `\n ${name}`;
+ // If we get here, then we know for a fact that we are stubbing object
+ // properties. Also, we do not care if `returnValue` was passed in here. If it
+ // is not passed in, then `returnValue` defaults to "stubbed". Otherwise, use
+ // the value of `returnValue`.
+ if (typeof obj === "object" && dataMember !== undefined) {
+ // If we are stubbing a method, then make sure the method is still callable
+ if (typeof obj[dataMember] === "function") {
+ Object.defineProperty(obj, dataMember, {
+ value: () => returnValue !== undefined ? returnValue : "stubbed",
+ writable: true,
+ });
} else {
- if (this.test_suite_in_progress != this.passed_in_test_suite) {
- this.test_suite_in_progress = this.passed_in_test_suite;
- newName = `${"\u0008".repeat(name.length + extraChars)}` +
- ` ${this.passed_in_test_suite}` +
- `${" ".repeat(name.length + extraChars)}` +
- `\n ${name}`;
- } else {
- newName = `${"\u0008".repeat(name.length + extraChars)}` +
- ` ${name}`;
- }
+ // If we are stubbing a property, then just reassign the property
+ Object.defineProperty(obj, dataMember, {
+ value: returnValue !== undefined ? returnValue : "stubbed",
+ writable: true,
+ });
}
-
- return newName;
- }
-
- /**
- * 'Empty' this object. After calling this, Rhum should be ready for another
- * test plan.
- */
- protected deconstruct(): void {
- this.passed_in_test_suite = "";
- this.passed_in_test_plan = "";
- this.test_plan_in_progress = "";
- this.test_suite_in_progress = "";
- this.plan = { suites: {} };
}
}
-
-/**
- * An instance of the RhumRunner.
- *
- * const Rhum = new RhumRunner();
- */
-export const Rhum = new RhumRunner();
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..8d4ed42a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@drashland/rhum",
+ "version": "2.0.0",
+ "description": "A test double library",
+ "main": "./lib/cjs/mod.js",
+ "types": "./lib/cjs/mod.d.ts",
+ "repository": "git@github.com:drashland/rhum.git",
+ "author": "Drash Land",
+ "license": "MIT",
+ "scripts": {
+ "build": "console/build_esm_lib && yarn build:cjs && yarn build:esm",
+ "build:windows": "bash console/build_esm_lib && yarn build:cjs && yarn build:esm",
+ "build:cjs": "tsc --project tsconfig.cjs.json",
+ "build:esm": "tsc --project tsconfig.esm.json",
+ "check": "rm -rf node_modules/ && rm yarn.lock && yarn install && yarn build && yarn test",
+ "test": "jest tests"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "7.x",
+ "@types/jest": "27.x",
+ "@types/node": "16.x",
+ "babel-jest": "27.x",
+ "jest": "27.x",
+ "ts-jest": "27.x",
+ "ts-node": "10.x",
+ "typescript": "4.x"
+ },
+ "files": [
+ "./lib"
+ ]
+}
diff --git a/src/fake/fake_builder.ts b/src/fake/fake_builder.ts
new file mode 100644
index 00000000..c3818088
--- /dev/null
+++ b/src/fake/fake_builder.ts
@@ -0,0 +1,281 @@
+import type { Constructor } from "../types.ts";
+import type { IFake } from "../interfaces.ts";
+import { createFake } from "./fake_mixin.ts";
+import { PreProgrammedMethod } from "../pre_programmed_method.ts";
+
+/**
+ * Builder to help build a fake object. This does all of the heavy-lifting to
+ * create a fake object.
+ */
+export class FakeBuilder {
+ /**
+ * The class to fake (should be constructable).
+ */
+ #constructor_fn: Constructor;
+
+ /**
+ * A list of arguments the class constructor takes.
+ */
+ #constructor_args: unknown[] = [];
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct an object of this class.
+ *
+ * @param constructorFn - The class to fake (should be constructable).
+ */
+ constructor(constructorFn: Constructor) {
+ this.#constructor_fn = constructorFn;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Create the fake object.
+ *
+ * @returns The original object with capabilities from the fake class.
+ */
+ public create(): ClassToFake & IFake {
+ const original = new this.#constructor_fn(...this.#constructor_args);
+
+ const fake = createFake, ClassToFake>(
+ this.#constructor_fn,
+ );
+ fake.init(original, this.#getAllFunctionNames(original));
+
+ // Attach all of the original's properties to the fake
+ this.#getAllPropertyNames(original).forEach((property: string) => {
+ this.#addOriginalObjectPropertyToFakeObject(
+ original,
+ fake,
+ property,
+ );
+ });
+
+ // Attach all of the original's functions to the fake
+ this.#getAllFunctionNames(original).forEach((method: string) => {
+ this.#addOriginalObjectMethodToFakeObject(
+ original,
+ fake,
+ method,
+ );
+ });
+
+ return fake as ClassToFake & IFake;
+ }
+
+ /**
+ * Before constructing the fake object, track any constructor function args
+ * that need to be passed in when constructing the fake object.
+ *
+ * @param args - A rest parameter of arguments that will get passed in to the
+ * constructor function of the object being faked.
+ *
+ * @returns `this` so that methods in this class can be chained.
+ */
+ public withConstructorArgs(...args: unknown[]): this {
+ this.#constructor_args = args;
+ return this;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add an original object's method to a fake object without doing anything
+ * else.
+ *
+ * @param original - The original object containing the method to fake.
+ * @param fake - The fake object receiving the method to fake.
+ * @param method - The name of the method to fake -- callable via
+ * `fake[method](...)`.
+ */
+ #addMethodToFakeObject(
+ original: ClassToFake,
+ fake: IFake,
+ method: string,
+ ): void {
+ Object.defineProperty(fake, method, {
+ value: original[method as keyof ClassToFake],
+ });
+ }
+
+ /**
+ * Add an original object's method to a fake object -- determining whether the
+ * method should or should not be trackable.
+ *
+ * @param original - The original object containing the method to add.
+ * @param fake - The fake object receiving the method.
+ * @param method - The name of the method to fake -- callable via
+ * `fake[method](...)`.
+ */
+ #addOriginalObjectMethodToFakeObject(
+ original: ClassToFake,
+ fake: IFake,
+ method: string,
+ ): void {
+ const nativeMethods = [
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "constructor",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toString",
+ "valueOf",
+ ];
+
+ // If this is a native method, then do not do anything fancy. Just add it to
+ // the fake.
+ if (nativeMethods.indexOf(method as string) !== -1) {
+ return this.#addMethodToFakeObject(
+ original,
+ fake,
+ method,
+ );
+ }
+
+ // Otherwise, make the method trackable via `.calls` usage.
+ this.#addTrackableMethodToFakeObject(
+ original,
+ fake,
+ method as keyof ClassToFake,
+ );
+ }
+
+ /**
+ * Add an original object's property to a fake object.
+ *
+ * @param original The original object containing the property.
+ * @param fake The fake object receiving the property.
+ * @param property The name of the property -- retrievable via
+ * `fake[property]`.
+ */
+ #addOriginalObjectPropertyToFakeObject(
+ original: ClassToFake,
+ fake: IFake,
+ property: string,
+ ): void {
+ const desc = Object.getOwnPropertyDescriptor(original, property) ??
+ Object.getOwnPropertyDescriptor(
+ this.#constructor_fn.prototype,
+ property,
+ );
+
+ // If we do not have a desc, then we have no idea what the value should be.
+ // Also, we have no idea what we are copying, so we should just not do it.
+ if (!desc) {
+ return;
+ }
+
+ // Basic property (e.g., public test = "hello"). We do not handle get() and
+ // set() because those are handled by the fake mixin.
+ if (("value" in desc)) {
+ Object.defineProperty(fake, property, {
+ value: desc.value,
+ writable: true,
+ });
+ }
+ }
+
+ /**
+ * Add a trackable method to a fake object. A trackable method is one that can
+ * be verified using `fake.calls[someMethod]`.
+ *
+ * @param original - The original object containing the method to add.
+ * @param fake - The fake object receiving the method.
+ * @param method - The name of the method.
+ */
+ #addTrackableMethodToFakeObject(
+ original: ClassToFake,
+ fake: IFake,
+ method: keyof ClassToFake,
+ ): void {
+ Object.defineProperty(fake, method, {
+ value: (...args: unknown[]) => {
+ // Make sure the method calls its original self
+ const methodToCall =
+ (original[method as keyof ClassToFake] as unknown as (
+ ...params: unknown[]
+ ) => unknown);
+
+ // We need to check if the method was pre-preprogrammed to return
+ // something. If it was, then we make sure that this method we are
+ // currently defining returns that pre-programmed value.
+ if (methodToCall instanceof PreProgrammedMethod) {
+ if (methodToCall.will_throw) {
+ throw methodToCall.error;
+ }
+ return methodToCall.return;
+ }
+
+ // When method calls its original self, let the `this` context of the
+ // original be the fake. Reason being the fake has tracking and the
+ // original does not.
+ const bound = methodToCall.bind(fake);
+
+ // Use `return` because the original function could return a value
+ return bound(...args);
+ },
+ });
+ }
+
+ /**
+ * Get all properties from the original so they can be added to the fake.
+ *
+ * @param obj - The object that will be faked.
+ *
+ * @returns An array of the object's properties.
+ */
+ #getAllPropertyNames(obj: ClassToFake): string[] {
+ let functions: string[] = [];
+ let clone = obj;
+ do {
+ functions = functions.concat(Object.getOwnPropertyNames(clone));
+ } while ((clone = Object.getPrototypeOf(clone)));
+
+ return functions.sort().filter(
+ function (e: string, i: number, arr: unknown[]) {
+ if (
+ e != arr[i + 1] && typeof obj[e as keyof ClassToFake] != "function"
+ ) {
+ return true;
+ }
+ },
+ );
+ }
+
+ /**
+ * Get all functions from the original so they can be added to the fake.
+ *
+ * @param obj - The object that will be faked.
+ *
+ * @returns An array of the object's functions.
+ */
+ #getAllFunctionNames(obj: ClassToFake): string[] {
+ let functions: string[] = [];
+ let clone = obj;
+ do {
+ functions = functions.concat(Object.getOwnPropertyNames(clone));
+ } while ((clone = Object.getPrototypeOf(clone)));
+
+ return functions.sort().filter(
+ function (e: string, i: number, arr: unknown[]) {
+ if (
+ e != arr[i + 1] && typeof obj[e as keyof ClassToFake] == "function"
+ ) {
+ return true;
+ }
+ },
+ );
+ }
+}
diff --git a/src/fake/fake_mixin.ts b/src/fake/fake_mixin.ts
new file mode 100644
index 00000000..87cd4303
--- /dev/null
+++ b/src/fake/fake_mixin.ts
@@ -0,0 +1,66 @@
+import type { Constructor, MethodOf } from "../types.ts";
+import { PreProgrammedMethod } from "../pre_programmed_method.ts";
+import type { IFake } from "../interfaces.ts";
+
+class FakeError extends Error {}
+
+export function createFake(
+ OriginalClass: OriginalConstructor,
+): IFake {
+ const Original = OriginalClass as unknown as Constructor<
+ // deno-lint-ignore no-explicit-any
+ (...args: any[]) => any
+ >;
+ return new class FakeExtension extends Original {
+ /**
+ * Helper property to see that this is a fake object and not the original.
+ */
+ is_fake = true;
+
+ /**
+ * The original object that this class creates a fake of.
+ */
+ #original!: OriginalObject;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @param original - The original object to fake.
+ */
+ public init(original: OriginalObject) {
+ this.#original = original;
+ }
+
+ /**
+ * Pre-program a method on the original to return a specific value.
+ *
+ * @param methodName The method name on the original.
+ * @returns A pre-programmed method that will be called instead of original.
+ */
+ public method(
+ methodName: MethodOf,
+ ): PreProgrammedMethod {
+ const methodConfiguration = new PreProgrammedMethod<
+ OriginalObject,
+ ReturnValueType
+ >(
+ methodName,
+ );
+
+ if (!((methodName as string) in this.#original)) {
+ throw new FakeError(
+ `Method "${methodName}" does not exist.`,
+ );
+ }
+
+ Object.defineProperty(this.#original, methodName, {
+ value: methodConfiguration,
+ writable: true,
+ });
+
+ return methodConfiguration;
+ }
+ }();
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 5f987bfe..72374642 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -1,121 +1,46 @@
-/**
- * @remarks
- * suites
- * An object of objects matching the ITestSuite interface.
- *
- * after_all_suite_hook?
- * A callback function to execute after all test suites.
- *
- * after_each_suite_hook?
- * A callback function to execute after each test suite.
- *
- * before_all_suite_hook?
- * A callback function to execute before all test suites.
- *
- * before_each_suite_hook?
- * A callback function to execute before each test suite.
- *
- * Example below ...
- *
- * {
- * suites: {
- * "My Suite": {
- * cases: [
- * {
- * name: "My Case",
- * new_name: this.formatTestCaseName(name),
- * testFn: Function
- * },
- * ...
- * ],
- * after_all_case_hook: Function,
- * after_each_case_hook: Function,
- * before_all_case_hook: Function,
- * before_each_case_hook: Function
- * },
- * ... // More suites allowed
- * },
- * after_all_suite_hook: Function;
- * after_each_suite_hook: Function;
- * before_all_suite_hook: Function;
- * before_each_suite_hook: Function;
- * }
- *
- * ... or ...
- *
- * {
- * suites: {
- * run(): {
- * cases: [Array],
- * after_all_case_hook: [Function],
- * before_all_case_hook: [Function],
- * before_each_case_hook: [Function],
- * after_each_case_hook: [Function]
- * },
- * close(): {
- * cases: [Array],
- * after_all_case_hook: [Function],
- * before_all_case_hook: [Function],
- * before_each_case_hook: [Function],
- * after_each_case_hook: [Function]
- * }
- * },
- * before_each_suite_hook: [Function],
- * after_each_suite_hook: [Function],
- * after_all_suite_hook: [Function],
- * before_all_suite_hook: [Function]
- * }
- */
-export interface ITestPlan {
- suites: {
- [key: string]: ITestSuite; // "key" is the suite name
- };
- after_all_suite_hook?: () => void;
- after_each_suite_hook?: () => void;
- before_all_suite_hook?: () => void;
- before_each_suite_hook?: () => void;
-}
+import type { MethodCalls, MethodOf } from "./types.ts";
-/**
- * cases?
- * An array of objects matching the ITestCase interface.
- *
- * after_all_case_hook?
- * A callback function to execute after all test cases.
- *
- * after_each_case_hook?
- * A callback function to execute after each test case.
- *
- * before_all_case_hook?
- * A callback function to execute before all test cases.
- *
- * before_each_case_hook?
- * A callback function to execute before each test case.
- */
-export interface ITestSuite {
- cases?: ITestCase[];
- after_all_case_hook?: () => void;
- after_each_case_hook?: () => void;
- before_all_case_hook?: () => void;
- before_each_case_hook?: () => void;
+export interface IMethodExpectation {
+ toBeCalled(expectedCalls: number): void;
}
-/**
- * name
- * The name of the test case.
- *
- * new_name
- * The new name of the test. This is strictly for outputting purposes.
- * Deno's test runner outputs "test name of test" and we want to
- * overwrite that text. This new_name string helps us do that. See
- * formatTestCaseName() in mod.ts for more information.
- *
- * testFn
- * The test function. Ultimately, this gets passed as the second
- * argument of Deno.test().
- */
-export interface ITestCase {
+export interface IError {
name: string;
- new_name: string;
- testFn: () => void;
+ message?: string;
+}
+
+export interface IPreProgrammedMethod {
+ willReturn(returnValue: ReturnValue): void;
+ willThrow(error: IError): void;
+}
+
+export interface IFake {
+ is_fake: boolean;
+
+ init(
+ original: OriginalObject,
+ methodsToTrack: string[],
+ ): void;
+
+ method(
+ methodName: MethodOf,
+ ): IPreProgrammedMethod;
+}
+
+export interface IMock {
+ calls: MethodCalls;
+ is_mock: boolean;
+
+ init(
+ original: OriginalObject,
+ methodsToTrack: string[],
+ ): void;
+
+ expects(method: MethodOf): IMethodExpectation;
+
+ method(
+ methodName: MethodOf,
+ ): IPreProgrammedMethod;
+
+ verifyExpectations(): void;
}
diff --git a/src/mock/mock_builder.ts b/src/mock/mock_builder.ts
new file mode 100644
index 00000000..612fbdfc
--- /dev/null
+++ b/src/mock/mock_builder.ts
@@ -0,0 +1,286 @@
+import type { Constructor } from "../types.ts";
+import type { IMock } from "../interfaces.ts";
+import { createMock } from "./mock_mixin.ts";
+import { PreProgrammedMethod } from "../pre_programmed_method.ts";
+
+/**
+ * Builder to help build a mock object. This does all of the heavy-lifting to
+ * create a mock object. Its `create()` method returns an instance of `Mock`,
+ * which is basically an original object with added data members for verifying
+ * behavior.
+ */
+export class MockBuilder {
+ /**
+ * The class object passed into the constructor
+ */
+ #constructor_fn: Constructor;
+
+ /**
+ * A list of arguments the class constructor takes
+ */
+ #constructor_args: unknown[] = [];
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct an object of this class.
+ *
+ * @param constructorFn - The constructor function of the object to mock.
+ */
+ constructor(constructorFn: Constructor) {
+ this.#constructor_fn = constructorFn;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Create the mock object.
+ *
+ * @returns The original object with capabilities from the Mock class.
+ */
+ public create(): ClassToMock & IMock {
+ const original = new this.#constructor_fn(...this.#constructor_args);
+
+ const mock = createMock, ClassToMock>(
+ this.#constructor_fn,
+ );
+ mock.init(original, this.#getAllFunctionNames(original));
+
+ // Attach all of the original's properties to the mock
+ this.#getAllPropertyNames(original).forEach((property: string) => {
+ this.#addOriginalObjectPropertyToMockObject(
+ original,
+ mock,
+ property,
+ );
+ });
+
+ // Attach all of the original's functions to the mock
+ this.#getAllFunctionNames(original).forEach((method: string) => {
+ this.#addOriginalObjectMethodToMockObject(
+ original,
+ mock,
+ method,
+ );
+ });
+
+ return mock as ClassToMock & IMock;
+ }
+
+ /**
+ * Before constructing the mock object, track any constructor function args
+ * that need to be passed in when constructing the mock object.
+ *
+ * @param args - A rest parameter of arguments that will get passed in to the
+ * constructor function of the object being mocked.
+ *
+ * @returns `this` so that methods in this class can be chained.
+ */
+ public withConstructorArgs(...args: unknown[]): this {
+ this.#constructor_args = args;
+ return this;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add an original object's method to a mock object without doing anything
+ * else.
+ *
+ * @param original - The original object containing the method to mock.
+ * @param mock - The mock object receiving the method to mock.
+ * @param method - The name of the method to mock -- callable via
+ * `mock[method](...)`.
+ */
+ #addMethodToMockObject(
+ original: ClassToMock,
+ mock: IMock,
+ method: string,
+ ): void {
+ Object.defineProperty(mock, method, {
+ value: original[method as keyof ClassToMock],
+ });
+ }
+
+ /**
+ * Add an original object's method to a mock object -- determining whether the
+ * method should or should not be trackable.
+ *
+ * @param original - The original object containing the method to add.
+ * @param mock - The mock object receiving the method.
+ * @param method - The name of the method to mock -- callable via
+ * `mock[method](...)`.
+ */
+ #addOriginalObjectMethodToMockObject(
+ original: ClassToMock,
+ mock: IMock,
+ method: string,
+ ): void {
+ const nativeMethods = [
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "constructor",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toString",
+ "valueOf",
+ ];
+
+ // If this is a native method, then do not do anything fancy. Just add it to
+ // the mock.
+ if (nativeMethods.indexOf(method as string) !== -1) {
+ return this.#addMethodToMockObject(
+ original,
+ mock,
+ method,
+ );
+ }
+
+ // Otherwise, make the method trackable via `.calls` usage.
+ this.#addTrackableMethodToMockObject(
+ original,
+ mock,
+ method as keyof ClassToMock,
+ );
+ }
+
+ /**
+ * Add an original object's property to a mock object.
+ *
+ * @param original The original object containing the property.
+ * @param mock The mock object receiving the property.
+ * @param property The name of the property -- retrievable via
+ * `mock[property]`.
+ */
+ #addOriginalObjectPropertyToMockObject(
+ original: ClassToMock,
+ mock: IMock,
+ property: string,
+ ): void {
+ const desc = Object.getOwnPropertyDescriptor(original, property) ??
+ Object.getOwnPropertyDescriptor(
+ this.#constructor_fn.prototype,
+ property,
+ );
+
+ // If we do not have a desc, then we have no idea what the value should be.
+ // Also, we have no idea what we are copying, so we should just not do it.
+ if (!desc) {
+ return;
+ }
+
+ // Basic property (e.g., public test = "hello"). We do not handle get() and
+ // set() because those are handled by the mock mixin.
+ if (("value" in desc)) {
+ Object.defineProperty(mock, property, {
+ value: desc.value,
+ writable: true,
+ });
+ }
+ }
+
+ /**
+ * Add a trackable method to a mock object. A trackable method is one that can
+ * be verified using `mock.calls[someMethod]`.
+ *
+ * @param original - The original object containing the method to add.
+ * @param mock - The mock object receiving the method.
+ * @param method - The name of the method.
+ */
+ #addTrackableMethodToMockObject(
+ original: ClassToMock,
+ mock: IMock,
+ method: keyof ClassToMock,
+ ): void {
+ Object.defineProperty(mock, method, {
+ value: (...args: unknown[]) => {
+ // Track that this method was called
+ mock.calls[method]++;
+
+ // Make sure the method calls its original self
+ const methodToCall =
+ (original[method as keyof ClassToMock] as unknown as (
+ ...params: unknown[]
+ ) => unknown);
+
+ // We need to check if the method was pre-preprogrammed to return
+ // something. If it was, then we make sure that this method we are
+ // currently defining returns that pre-programmed value.
+ if (methodToCall instanceof PreProgrammedMethod) {
+ if (methodToCall.will_throw) {
+ throw methodToCall.error;
+ }
+ return methodToCall.return;
+ }
+
+ // When method calls its original self, let the `this` context of the
+ // original be the mock. Reason being the mock has tracking and the
+ // original does not.
+ const bound = methodToCall.bind(mock);
+
+ // Use `return` because the original function could return a value
+ return bound(...args);
+ },
+ });
+ }
+
+ /**
+ * Get all properties from the original so they can be added to the mock.
+ *
+ * @param obj - The object that will be mocked.
+ *
+ * @returns An array of the object's properties.
+ */
+ #getAllPropertyNames(obj: ClassToMock): string[] {
+ let functions: string[] = [];
+ let clone = obj;
+ do {
+ functions = functions.concat(Object.getOwnPropertyNames(clone));
+ } while ((clone = Object.getPrototypeOf(clone)));
+
+ return functions.sort().filter(
+ function (e: string, i: number, arr: unknown[]) {
+ if (
+ e != arr[i + 1] && typeof obj[e as keyof ClassToMock] != "function"
+ ) {
+ return true;
+ }
+ },
+ );
+ }
+
+ /**
+ * Get all functions from the original so they can be added to the mock.
+ *
+ * @param obj - The object that will be mocked.
+ *
+ * @returns An array of the object's functions.
+ */
+ #getAllFunctionNames(obj: ClassToMock): string[] {
+ let functions: string[] = [];
+ let clone = obj;
+ do {
+ functions = functions.concat(Object.getOwnPropertyNames(clone));
+ } while ((clone = Object.getPrototypeOf(clone)));
+
+ return functions.sort().filter(
+ function (e: string, i: number, arr: unknown[]) {
+ if (
+ e != arr[i + 1] && typeof obj[e as keyof ClassToMock] == "function"
+ ) {
+ return true;
+ }
+ },
+ );
+ }
+}
diff --git a/src/mock/mock_mixin.ts b/src/mock/mock_mixin.ts
new file mode 100644
index 00000000..8b642dd1
--- /dev/null
+++ b/src/mock/mock_mixin.ts
@@ -0,0 +1,160 @@
+import type { Constructor, MethodCalls, MethodOf } from "../types.ts";
+import { PreProgrammedMethod } from "../pre_programmed_method.ts";
+import type { IMock } from "../interfaces.ts";
+
+class MockError extends Error {}
+
+class MethodExpectation {
+ #method_name: MethodOf;
+ #expected_calls = 0;
+
+ get method_name(): MethodOf {
+ return this.#method_name;
+ }
+
+ get expected_calls(): number {
+ return this.#expected_calls;
+ }
+
+ constructor(methodName: MethodOf) {
+ this.#method_name = methodName;
+ }
+
+ public toBeCalled(expectedCalls: number) {
+ this.#expected_calls = expectedCalls;
+ }
+}
+
+export function createMock(
+ OriginalClass: OriginalConstructor,
+): IMock {
+ const Original = OriginalClass as unknown as Constructor<
+ // deno-lint-ignore no-explicit-any
+ (...args: any[]) => any
+ >;
+ return new class MockExtension extends Original {
+ /**
+ * Helper property to see that this is a mock object and not the original.
+ */
+ is_mock = true;
+
+ /**
+ * Property to track method calls.
+ */
+ #calls!: MethodCalls;
+
+ /**
+ * An array of expectations to verify (if any).
+ */
+ #expectations: MethodExpectation[] = [];
+
+ /**
+ * The original object that this class creates a mock of.
+ */
+ #original!: OriginalObject;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - GETTERS / SETTERS ///////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ get calls(): MethodCalls {
+ return this.#calls;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @param original - The original object to mock.
+ * @param methodsToTrack - The original object's method to make trackable.
+ */
+ public init(original: OriginalObject, methodsToTrack: string[]) {
+ this.#original = original;
+ this.#calls = this.#constructCallsProperty(methodsToTrack);
+ }
+
+ /**
+ * Create a method expectation, which is basically asserting calls.
+ *
+ * @param method - The method to create an expectation for.
+ * @returns A method expectation.
+ */
+ public expects(
+ method: MethodOf,
+ ): MethodExpectation {
+ const expectation = new MethodExpectation(method);
+ this.#expectations.push(expectation);
+ return expectation;
+ }
+
+ /**
+ * Pre-program a method on the original to return a specific value.
+ *
+ * @param methodName The method name on the original.
+ * @returns A pre-programmed method that will be called instead of original.
+ */
+ public method(
+ methodName: MethodOf,
+ ): PreProgrammedMethod {
+ const methodConfiguration = new PreProgrammedMethod<
+ OriginalObject,
+ ReturnValueType
+ >(
+ methodName,
+ );
+
+ if (!((methodName as string) in this.#original)) {
+ throw new MockError(
+ `Method "${methodName}" does not exist.`,
+ );
+ }
+
+ Object.defineProperty(this.#original, methodName, {
+ value: methodConfiguration,
+ writable: true,
+ });
+
+ return methodConfiguration;
+ }
+
+ /**
+ * Verify all expectations created in this mock.
+ */
+ public verifyExpectations(): void {
+ this.#expectations.forEach((e: MethodExpectation) => {
+ const expectedCalls = e.expected_calls;
+ const actualCalls = this.#calls[e.method_name];
+ if (expectedCalls !== actualCalls) {
+ throw new MockError(
+ `Method "${e.method_name}" expected ${expectedCalls} call(s), but received ${actualCalls} call(s).`,
+ );
+ }
+ });
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct the calls property. Only construct it, do not set it. The
+ * constructor will set it.
+ * @param methodsToTrack - All of the methods on the original object to make
+ * trackable.
+ * @returns - Key-value object where the key is the method name and the value
+ * is the number of calls. All calls start at 0.
+ */
+ #constructCallsProperty(
+ methodsToTrack: string[],
+ ): Record {
+ const calls: Partial> = {};
+
+ methodsToTrack.map((key: string) => {
+ calls[key as keyof OriginalObject] = 0;
+ });
+
+ return calls as Record;
+ }
+ }();
+}
diff --git a/src/mock_builder.ts b/src/mock_builder.ts
deleted file mode 100644
index e0a31c0a..00000000
--- a/src/mock_builder.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import type { Constructor, Mocked } from "./types.ts";
-
-export class MockBuilder {
- /**
- * Properties of the class that is passed in
- */
- protected properties: string[] = [];
-
- /**
- * Functions of the class passed in
- */
- protected functions: string[] = [];
-
- /**
- * The class object passed into the constructor
- */
- protected constructor_fn: Constructor;
-
- /**
- * A list of arguments the class constructor takes
- */
- protected constructor_args: unknown[] = [];
-
- //////////////////////////////////////////////////////////////////////////////
- // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////
-
- /**
- * Construct an object of this class.
- *
- * @param constructorFn - The object's constructor function to instantiate.
- */
- constructor(constructorFn: Constructor) {
- this.constructor_fn = constructorFn;
- }
-
- //////////////////////////////////////////////////////////////////////////////
- // FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////
-
- /**
- * Create the mock object.
- *
- * @returns A mocked object.
- */
- public create(): Mocked {
- // deno-lint-ignore no-explicit-any
- const mock: Mocked = {
- calls: {},
- is_mock: true,
- };
- const original = new this.constructor_fn(...this.constructor_args);
-
- // Attach all of the original's properties to the mock
- this.getAllProperties(original).forEach((property: string) => {
- const desc = Object.getOwnPropertyDescriptor(original, property) ??
- Object.getOwnPropertyDescriptor(
- this.constructor_fn.prototype,
- property,
- );
-
- // Handle getter/setters differently so we can initialise their default
- // getter value
- if (typeof desc!.get === "function") {
- // deno-lint-ignore ban-ts-comment
- //@ts-ignore
- mock[property] = original[property];
- return; // continue
- }
-
- mock[property] = desc!.value;
- });
-
- // Attach all of the original's functions to the mock
- this.getAllFunctions(original).forEach((method: string) => {
- const nativeMethods = [
- "__defineGetter__",
- "__defineSetter__",
- "__lookupGetter__",
- "__lookupSetter__",
- "constructor",
- "hasOwnProperty",
- "isPrototypeOf",
- "propertyIsEnumerable",
- "toLocaleString",
- "toString",
- "valueOf",
- ];
-
- if (nativeMethods.indexOf(method) == -1) {
- if (!mock.calls[method]) {
- mock.calls[method] = 0;
- }
- mock[method] = function (...args: unknown[]) {
- // Add tracking
- mock.calls[method]++;
-
- // Make sure the method calls its original self
- let unbound = (original[method as keyof T] as unknown as (
- ...params: unknown[]
- ) => unknown);
-
- // When method calls its original self, let `this` be the mock. Reason
- // being the mock has tracking and the original does not.
- const bound = unbound.bind(mock);
-
- return bound(...args);
- };
- } else {
- // copy nativeMethod directly without mocking
- mock[method] = original[method as keyof T];
- }
- });
-
- return mock;
- }
-
- /**
- * Before constructing the mock object, track any constructur function args
- * that need to be passed in when constructing the mock object.
- *
- * @param args - A rest parameter of arguments that will get passed in to
- * the constructor function of the class being mocked.
- *
- * @returns `this` so that methods in this class can be chained.
- */
- public withConstructorArgs(...args: unknown[]): this {
- this.constructor_args = args;
- return this;
- }
-
- //////////////////////////////////////////////////////////////////////////////
- // FILE MARKER - METHODS - PROTECTED /////////////////////////////////////////
- //////////////////////////////////////////////////////////////////////////////
-
- /**
- * Get all properties--public, protected, private--from the object that will
- * be mocked.
- *
- * @param obj - The object that will be mocked.
- *
- * @returns An array of the object's properties.
- */
- protected getAllProperties(obj: T): string[] {
- let functions: string[] = [];
- let clone = obj;
- do {
- functions = functions.concat(Object.getOwnPropertyNames(clone));
- } while ((clone = Object.getPrototypeOf(clone)));
-
- return functions.sort().filter(
- function (e: string, i: number, arr: unknown[]) {
- if (
- e != arr[i + 1] && typeof obj[e as keyof T] != "function"
- ) {
- return true;
- }
- },
- );
- }
-
- /**
- * Get all functions--public, protected, private--from the object that will be
- * mocked.
- *
- * @param obj - The object that will be mocked.
- *
- * @returns An array of the object's functions.
- */
- protected getAllFunctions(obj: T): string[] {
- let functions: string[] = [];
- let clone = obj;
- do {
- functions = functions.concat(Object.getOwnPropertyNames(clone));
- } while ((clone = Object.getPrototypeOf(clone)));
-
- return functions.sort().filter(
- function (e: string, i: number, arr: unknown[]) {
- if (
- e != arr[i + 1] && typeof obj[e as keyof T] == "function"
- ) {
- return true;
- }
- },
- );
- }
-}
diff --git a/src/pre_programmed_method.ts b/src/pre_programmed_method.ts
new file mode 100644
index 00000000..9ae32f85
--- /dev/null
+++ b/src/pre_programmed_method.ts
@@ -0,0 +1,92 @@
+import type { MethodOf } from "./types.ts";
+import type { IError } from "./interfaces.ts";
+
+class PreProgrammedMethodError extends Error {}
+
+/**
+ * Class that allows to be a "stand-in" for a method. For example, when used in
+ * a mock object, the mock object can replace methods with pre-programmed
+ * methods (using this class), and have a system under test use the
+ * pre-programmed methods.
+ */
+export class PreProgrammedMethod {
+ #method_name: MethodOf;
+ #will_throw = false;
+ #will_return = false;
+ #return?: ReturnValue;
+ #error?: IError;
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @param methodName - The name of the method to program. Must be a method of
+ * the original object in question.
+ */
+ constructor(methodName: MethodOf) {
+ this.#method_name = methodName;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - GETTERS / SETTERS //////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ get error(): void {
+ if (!this.#will_throw) {
+ throw new PreProgrammedMethodError(
+ `Pre-programmed method "${this.#method_name}" is not set up to throw an error.`,
+ );
+ }
+ if (this.#error === undefined) {
+ throw new PreProgrammedMethodError(
+ `Pre-programmed method "${this.#method_name}" is set up to throw an error, but no error was provided.`,
+ );
+ }
+
+ throw this.#error;
+ }
+
+ get return(): ReturnValue {
+ if (this.#return === undefined) {
+ throw new PreProgrammedMethodError(
+ `Pre-programmed method "${this.#method_name}" does not have a return value.`,
+ );
+ }
+
+ return this.#return;
+ }
+
+ get will_return(): boolean {
+ return this.#will_return;
+ }
+
+ get will_throw(): boolean {
+ return this.#will_throw;
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // FILE MARKER - METHODS - PUBLIC ///////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Pre-program this method to return the given value.
+
+ * @param returnValue The value that should be returned when this object is
+ * being used in place of an original method.
+ */
+ public willReturn(returnValue: ReturnValue): void {
+ this.#will_return = true;
+ this.#return = returnValue;
+ }
+
+ /**
+ * Pre-program this method to throw the given error.
+ *
+ * @param error - The error to throw.
+ */
+ public willThrow(error: IError): void {
+ this.#will_throw = true;
+ this.#error = error;
+ }
+}
diff --git a/src/rhum_asserts.ts b/src/rhum_asserts.ts
deleted file mode 100644
index 981caac8..00000000
--- a/src/rhum_asserts.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { StdAsserts } from "../deps.ts";
-
-// Saving this so i don't have to type it out for the 10th time
-// export const asserts = {
-// AssertionError: StdAsserts.AssertionError,
-// _format: StdAsserts._format,
-// assert: StdAsserts.assert,
-// assertArrayIncludes: StdAsserts.assertArrayIncludes,
-// assertEquals: StdAsserts.assertEquals,
-// assertNotEquals: StdAsserts.assertNotEquals,
-// assertNotMatch: StdAsserts.assertNotMatch,
-// assertNotStrictEquals: StdAsserts.assertNotStrictEquals,
-// assertStrictEquals: StdAsserts.assertStrictEquals,
-// assertStringIncludes: StdAsserts.assertStringIncludes,
-// assertMatch: StdAsserts.assertMatch,
-// assertThrows: StdAsserts.assertThrows,
-// assertThrowsAsync: StdAsserts.assertThrowsAsync,
-// equal: StdAsserts.equal,
-// fail: StdAsserts.fail,
-// unimplemented: StdAsserts.unimplemented,
-// unreachable: StdAsserts.unreachable
-// };
-
-export const asserts = {
- ...StdAsserts,
- assertArrayContains: StdAsserts.assertArrayIncludes,
- assertStringContains: StdAsserts.assertStringIncludes,
-};
-
-export type assertions = keyof typeof asserts;
diff --git a/src/test_case.ts b/src/test_case.ts
deleted file mode 100644
index c02193bf..00000000
--- a/src/test_case.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-const encoder = new TextEncoder();
-import type { ITestCase, ITestPlan } from "./interfaces.ts";
-
-/**
- * A class to help create uniform test case objects.
- */
-export class TestCase {
- /**
- * The whole test plan for a given test
- */
- protected plan: ITestPlan;
-
- /**
- * @param plan - The test plan for a given test
- */
- constructor(plan: ITestPlan) {
- this.plan = plan;
- }
-
- /**
- * Runs the test plan and each test and hook
- */
- public async run() {
- // deno-lint-ignore no-prototype-builtins, eslint-ignore-next-line no-prototype-builtins
- if (this.plan.hasOwnProperty("suites") === false) {
- return;
- }
-
- // Track the execution of hooks
- let executedBeforeAllSuiteHook = false;
- let executedAfterAllSuiteHook = false;
-
- Object.keys(this.plan.suites).forEach((suiteName, suiteIndex) => {
- // Track the execution of hooks
- let executedBeforeEachSuiteHook = false;
- let executedAfterEachSuiteHook = false;
- let executedBeforeAllCaseHook = false;
- let executedAfterAllCaseHook = false;
-
- // Run cases
- this.plan!.suites[suiteName].cases!.forEach(
- async (c: ITestCase, caseIndex) => {
- const isLastCase =
- (this.plan!.suites[suiteName].cases!.length - 1) == caseIndex;
- const isLastSuite =
- (Object.keys(this.plan!.suites).length - 1) == suiteIndex;
- // Run the case - required to run like this because the
- // hooks need to be ran inside the Deno.test call. Deno.test seems to queue
- // the tests, meaning all hooks are ran, and **then** the tests are ran
- const hookAttachedTestFn = async () => {
- if (
- this.plan.before_all_suite_hook && !executedBeforeAllSuiteHook
- ) {
- await this.plan.before_all_suite_hook();
- executedBeforeAllSuiteHook = true;
- }
- if (
- this.plan.before_each_suite_hook && !executedBeforeEachSuiteHook
- ) {
- await this.plan.before_each_suite_hook();
- executedBeforeEachSuiteHook = true;
- }
- if (
- this.plan.suites[suiteName].before_all_case_hook &&
- !executedBeforeAllCaseHook
- ) {
- await this.plan.suites[suiteName].before_all_case_hook!();
- executedBeforeAllCaseHook = true;
- }
- if (this.plan.suites[suiteName].before_each_case_hook) {
- await this.plan.suites[suiteName].before_each_case_hook!();
- }
-
- await c.testFn();
-
- if (this.plan.suites[suiteName].after_each_case_hook) {
- await this.plan.suites[suiteName].after_each_case_hook!();
- }
- if (
- this.plan.suites[suiteName].after_all_case_hook &&
- !executedAfterAllCaseHook && isLastCase
- ) {
- await this.plan.suites[suiteName].after_all_case_hook!();
- executedAfterAllCaseHook = true;
- }
- if (
- this.plan.after_each_suite_hook && !executedAfterEachSuiteHook
- ) {
- await this.plan.after_each_suite_hook();
- executedAfterEachSuiteHook = true;
- }
- if (
- this.plan.after_all_suite_hook && !executedAfterAllSuiteHook &&
- isLastSuite
- ) {
- await this.plan.after_all_suite_hook();
- executedAfterAllSuiteHook = true;
- }
- };
- // (ebebbington) To stop the output of test running being horrible
- // in the CI, we will only display the new name which should be
- // "plan | suite " case", as opposed to the "super saiyan"
- // version. This name is generated differently inside `formatTestCaseName`
- // based on if the tests are being ran inside a CI job
- if (Deno.env.get("CI") === "true") {
- await Deno.test(c.new_name, async () => {
- await hookAttachedTestFn();
- });
- } else {
- await Deno.test(c.new_name, async () => {
- // Deno.stdout.writeSync(encoder.encode(c.new_name));
- await hookAttachedTestFn();
- });
- }
- },
- );
- });
- }
-}
diff --git a/src/types.ts b/src/types.ts
index 4c6125a6..17a6c0bd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,18 +1,20 @@
-export type TConstructorFunction = {
- new (...args: unknown[]): T;
- [key: string]: unknown;
-};
-
// deno-lint-ignore no-explicit-any
-export type Constructor = new (...args: any[]) => T;
+export type Constructor = new (...args: any[]) => T;
+
+export type MethodCalls