+# 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
+
-- Lightweight
-- Zero 3rd party dependencies
-- Simple and easy to use
-- Asynchronous support
-- 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).
-
-## Contributing
-
-Want to contribute? Follow the Contributing Guidelines
-[here](https://github.com/drashland/.github/blob/master/CONTRIBUTING.md).
-
-## License
-
-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/mod.ts b/mod.ts
index 3ee3ac39..53715a7d 100644
--- a/mod.ts
+++ b/mod.ts
@@ -1,63 +1,124 @@
-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";
/**
- * Get the mock builder to mock classes.
+ * Create a dummy.
*
- * @param constructorFn - The constructor function of the object to mock.
+ * 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.
+ */
+export function Dummy(constructorFn?: Constructor): T {
+ const dummy = Object.create({});
+ Object.setPrototypeOf(dummy, constructorFn ?? Object);
+ return dummy;
+}
+
+/**
+ * Get the builder to create fake objects.
*
- * Returns an instance of the MockBuilder class.
+ * 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)."
*
- * class ToBeMocked { ... }
+ * @param constructorFn - The constructor function of the object to fake.
*
- * const mock = Rhum
- * .mock(ToBeMocked)
- * .withConstructorArgs("someArg") // if the class to be mocked has a constructor and it requires args
- * .create();
+ * @returns Instance of `FakeBuilder`.
*/
-export const Mock = (constructorFn: Constructor): MockBuilder => {
- return new MockBuilder(constructorFn);
-};
+export function Fake(constructorFn: Constructor): FakeBuilder {
+ return new FakeBuilder(constructorFn);
+}
/**
- * Stub a member of an object.
+ * Get the builder to create mocked objects.
*
- * @param obj -The object containing the member to stub.
- * @param member -The member to stub.
- * @param value - The return value of the stubbed member.
+ * 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."
*
- * 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.
+ * @param constructorFn - The constructor function of the object to mock.
*
- * class MyObject {
- * public some_property = "someValue";
- * }
+ * @returns Instance of `MockBuilder`.
+ */
+export function Mock(constructorFn: Constructor): MockBuilder {
+ return new MockBuilder(constructorFn);
+}
+
+/**
+ * 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.
*
- * // Define the object that will have stubbed members as a stubbed object
- * const myStubbedObject = Rhum.stubbed(new MyObject());
+ * @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.
*
- * // Stub the object's some_property property to a certain value
- * myStubbedObject.stub("some_property", "this property is now stubbed");
+ * 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."
*
- * // Assert that the property was stubbed
- * Rhum.asserts.assertEquals(myStubbedObject.some_property, "this property is now stubbed");
+ * @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 const Stub = (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,
- });
- };
+export function Stub(
+ obj?: T,
+ dataMember?: keyof T,
+ returnValue?: R,
+): unknown {
+ if (obj === undefined) {
+ return function stubbed() {
+ return "stubbed";
+ };
+ }
- return obj as Stubbed;
-};
+ // 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 we are stubbing a property, then just reassign the property
+ Object.defineProperty(obj, dataMember, {
+ value: returnValue !== undefined ? returnValue : "stubbed",
+ writable: true,
+ });
+ }
+ }
+}
diff --git a/src/fake/fake_builder.ts b/src/fake/fake_builder.ts
new file mode 100644
index 00000000..556b7b97
--- /dev/null
+++ b/src/fake/fake_builder.ts
@@ -0,0 +1,283 @@
+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.includes(method as string)) {
+ 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);
+
+ // console.log(`calling bound()`);
+
+ // 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
new file mode 100644
index 00000000..72374642
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,46 @@
+import type { MethodCalls, MethodOf } from "./types.ts";
+
+export interface IMethodExpectation {
+ toBeCalled(expectedCalls: number): void;
+}
+
+export interface IError {
+ name: string;
+ 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..e3aff42a
--- /dev/null
+++ b/src/mock/mock_builder.ts
@@ -0,0 +1,288 @@
+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.includes(method as string)) {
+ 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);
+
+ // console.log(`calling bound()`);
+
+ // 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 174d8ac8..00000000
--- a/src/mock_builder.ts
+++ /dev/null
@@ -1,177 +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,
- );
- 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
- const 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/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