import type { Callable, Constructor, MethodOf } from "./src/types.ts"; import { MockBuilder } from "./src/mock/mock_builder.ts"; import { FakeBuilder } from "./src/fake/fake_builder.ts"; import { SpyBuilder } from "./src/spy/spy_builder.ts"; import { SpyStubBuilder } from "./src/spy/spy_stub_builder.ts"; import * as Interfaces from "./src/interfaces.ts"; export * as Types from "./src/types.ts"; export * as Interfaces from "./src/interfaces.ts"; //////////////////////////////////////////////////////////////////////////////// // FILE MARKER - DUMMY ///////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** * 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. */ export function Dummy<T>(constructorFn?: Constructor<T>): T { const dummy = Object.create({}); Object.setPrototypeOf(dummy, constructorFn ?? Object); return dummy; } //////////////////////////////////////////////////////////////////////////////// // FILE MARKER - FAKE ////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** * Get the builder to create fake objects. * * 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)." * * @param constructorFn - The constructor function of the object to fake. * * @returns Instance of `FakeBuilder`. */ export function Fake<T>(constructorFn: Constructor<T>): FakeBuilder<T> { return new FakeBuilder(constructorFn); } //////////////////////////////////////////////////////////////////////////////// // FILE MARKER - MOCK ////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** * Get the builder to create mocked objects. * * 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." * * @param constructorFn - The constructor function of the object to mock. * * @returns Instance of `MockBuilder`. */ export function Mock<T>(constructorFn: Constructor<T>): MockBuilder<T> { return new MockBuilder(constructorFn); } //////////////////////////////////////////////////////////////////////////////// // FILE MARKER - SPY /////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** * Create a spy out of a function expression. * * @param functionExpression - The function expression to turn into a spy. * @param returnValue - (Optional) The value the spy should return when called. * Defaults to "spy-stubbed". * * @returns The original function expression with spying capabilities. */ export function Spy< OriginalFunction extends Callable<ReturnValue>, ReturnValue, >( functionExpression: OriginalFunction, returnValue?: ReturnValue, ): Interfaces.ISpyStubFunctionExpression & OriginalFunction; /** * Create a spy out of an object's method. * * @param obj - The object containing the method to spy on. * @param dataMember - The method to spy on. * @param returnValue - (Optional) The value the spy should return when called. * Defaults to "spy-stubbed". * * @returns The original method with spying capabilities. */ export function Spy<OriginalObject, ReturnValue>( obj: OriginalObject, dataMember: MethodOf<OriginalObject>, returnValue?: ReturnValue, ): Interfaces.ISpyStubMethod; /** * Create spy out of a class. * * @param constructorFn - The constructor function of the object to spy on. * * @returns The original object with spying capabilities. */ export function Spy<OriginalClass>( constructorFn: Constructor<OriginalClass>, ): Interfaces.ISpy<OriginalClass> & OriginalClass; /** * Create a spy out of a class, class method, or function. * * Per Martin Fowler (based on Gerard Meszaros), "Spies are stubs that also * record some information based on how they were called. One form of this might * be an email service that records how many messages it was sent." * * @param original - The original to turn into a spy. * @param methodOrReturnValue - (Optional) If creating a spy out of an object's method, then * this would be the method name. If creating a spy out of a function * expression, then this would be the return value. * @param returnValue - (Optional) If creating a spy out of an object's method, then * this would be the return value. */ export function Spy<OriginalObject, ReturnValue>( original: unknown, methodOrReturnValue?: unknown, returnValue?: unknown, ): unknown { if (typeof original === "function") { // If the function has the prototype field, the it's a constructor function. // // Examples: // class Hello { } // function Hello() { } // if ("prototype" in original) { return new SpyBuilder(original as Constructor<OriginalObject>).create(); } // Otherwise, it's just a function. // // Example: // const hello = () => "world"; // // Not that function declarations (e.g., function hello() { }) will have // "prototype" and will go through the SpyBuilder() flow above. return new SpyStubBuilder(original as OriginalObject) .returnValue(methodOrReturnValue as ReturnValue) .createForFunctionExpression(); } // If we get here, then we are not spying on a class or function. We must be // spying on an object's method. return new SpyStubBuilder(original as OriginalObject) .method(methodOrReturnValue as MethodOf<OriginalObject>) .returnValue(returnValue as ReturnValue) .createForObjectMethod(); } //////////////////////////////////////////////////////////////////////////////// // FILE MARKER - STUB ////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /** * Create a stub function that returns "stubbed". * * @returns A function that returns "stubbed". */ export function Stub<OriginalObject>(): () => "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<OriginalObject, ReturnValue>( obj: OriginalObject, dataMember: keyof OriginalObject, returnValue?: ReturnValue, ): void; /** * 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<OriginalObject, ReturnValue>( obj?: OriginalObject, dataMember?: keyof OriginalObject, returnValue?: ReturnValue, ): unknown { if (obj === null) { throw new Error(`Cannot create a stub using Stub(null)`); } if (obj === undefined) { return function stubbed() { return "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 "spy-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, }); } } }