Skip to content

Commit

Permalink
tmp
Browse files Browse the repository at this point in the history
  • Loading branch information
crookse committed Apr 24, 2022
1 parent 4efac0c commit b9d7beb
Show file tree
Hide file tree
Showing 12 changed files with 854 additions and 533 deletions.
106 changes: 90 additions & 16 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Constructor, StubReturnValue } from "./src/types.ts";
import type { Constructor, MethodOf, StubReturnValue, SpyReturnValue } 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 { SpyStub } from "./src/spy/spy_stub.ts";
import * as Interfaces from "./src/interfaces.ts";
export * as Types from "./src/types.ts";
export * as Interfaces from "./src/interfaces.ts";
Expand Down Expand Up @@ -70,33 +71,106 @@ export function Mock<T>(constructorFn: Constructor<T>): MockBuilder<T> {
return new MockBuilder(constructorFn);
}

export function Spy<T, R>(
obj: T,
dataMember: keyof T
): StubReturnValue<T, R>;
////////////////////////////////////////////////////////////////////////////////
// FILE MARKER - SPY ///////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

export function Spy<R>(
fn: (...args: unknown[]) => R,
returnValue?: R
): Interfaces.ISpyStub;

/**
* Create spy out of a class. Example:
*
* ```ts
* const spy = Spy(MyClass);
* const stubbedReturnValue = spy.someMethod(); // We called it, ...
* spy.verify("someMethod").toBeCalled(); // ... so we can verify it was called ...
* console.log(stubbedReturnValue === "stubbed"); // ... and that the return value is stubbed
* ```
*
* @param constructorFn - The constructor function to create a spy out of. This
* can be `class Something{ }` or `function Something() { }`.
*
* @returns Instance of `Spy`, which is an extension of the o.
*/
export function Spy<T>(
obj: Constructor<T>
constructorFn: Constructor<T>
): Interfaces.ISpy<T> & T;

export function Spy<T>(
/**
* Create a spy out of an object's data member. Example:
*
* ```ts
* const testSubject = new MyClass();
* const spyMethod = Spy(testSubject, "doSomething");
* // or const spyMethod = Spy(testSubject, "doSomething", "some return value");
*
* spyMethod.verify().toNotBeCalled(); // We can verify it was not called yet
*
* testSubject.doSomething(); // Now we called it, ...
* spyMethod.verify().toBeCalled(); // ... so we can verify it was called
* ```
*
* @param obj - The object containing the data member to spy on.
* @param dataMember - The data member to spy on.
* @param returnValue - (Optional) Make the data member return a specific value.
* Defaults to "stubbed" if not specified.
*
* @returns A spy stub that can be verified.
*/
export function Spy<T, R>(
obj: T,
dataMember?: keyof T
dataMember: MethodOf<T>,
returnValue?: R
): Interfaces.ISpyStub;

/**
* 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 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 Spy<T, R>(
obj: T,
dataMember?: MethodOf<T>,
returnValue?: R
): unknown {
if (dataMember) {
return Stub(obj, dataMember);
if (typeof obj === "function") {
// If the function has the prototype field, the it's a constructor function
if ("prototype" in obj) {
return new SpyBuilder(obj as unknown as Constructor<T>).create();
}
// Otherwise, its just a function
return new SpyStub(obj, undefined, dataMember)
}

if (typeof obj === "function" && ("prototype" in obj)) {
// @ts-ignore
return new SpyBuilder(obj).create();
if (dataMember !== undefined) {
return new SpyStub(obj, dataMember, returnValue);
}

throw new Error(`Incorrect use of Spy().`);
}

////////////////////////////////////////////////////////////////////////////////
// FILE MARKER - STUB //////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/**
* Create a stub function that returns "stubbed".
*/
export function Stub<T, R>(): () => "stubbed";

/**
* Take the given object and stub its given data member to return the given
* return value.
Expand All @@ -119,11 +193,11 @@ export function Stub<T, R>(
* 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
* @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.
* @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
* @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.
Expand Down
8 changes: 7 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ export class MethodVerificationError extends RhumError {
#expected_results: string;

/**
* @param message - The error message.
* @param message - The error message (to be shown in the stack trace).
* @param codeThatThrew - An example of the code (or the exact code) that
* caused this error to be thrown.
* @param actualResults - A message stating the actual results (to be show in
* the stack trace).
* @param expectedResults - A message stating the expected results (to be
* shown in the stack trace).
*/
constructor(
message: string,
Expand Down
6 changes: 3 additions & 3 deletions src/fake/fake_mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export function createFake<OriginalConstructor, OriginalObject>(
*/
#original!: OriginalObject;

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////

/**
* @param original - The original object to fake.
Expand Down
18 changes: 16 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export interface IMethodExpectation {
}

export interface IMethodVerification {
/**
* Verify that this method was called. Optionally, verify that it was called a
* specific number of times.
*
* @param expectedCalls - (Optional) The number of calls this method is
* expected to have received. If not provided, then the verification process
* will assume "just verify that the method was called" instead of verifying
* that it was called a specific number of times.
*
* @returns `this` To allow method chaining.
*/
toBeCalled(expectedCalls?: number): this;
toBeCalledWithArgs(...args: unknown[]): this;
toBeCalledWithoutArgs(): this;
Expand Down Expand Up @@ -90,15 +101,18 @@ export interface IMock<OriginalObject> {
}

export interface ISpy<OriginalObject> {
calls: MethodCalls<OriginalObject>;
calls_arguments: MethodArguments<OriginalObject>;
is_spy: boolean;
stubbed_methods: Record<MethodOf<OriginalObject>, ISpyStub>;

verify(
methodName: MethodOf<OriginalObject>,
): IMethodVerification;
}

export interface ISpyStub {
verify(): IMethodVerification;
}

export interface ITestDouble<OriginalObject> {
init(
original: OriginalObject,
Expand Down
95 changes: 65 additions & 30 deletions src/method_verifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,55 @@ import { MethodVerificationError } from "./errors.ts";
* arguments, and so on.
*/
export class MethodVerifier<OriginalObject> {
/**
* The name of the method using this class. This is only used for display in
* error stack traces if this class throws.
*/
#method_name: MethodOf<OriginalObject>;

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - CONSTRUCTOR /////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/**
* @param methodName - See this#method_name.
*/
constructor(methodName: MethodOf<OriginalObject>) {
this.#method_name = methodName;
}

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PUBLIC ////////////////////////////////////////////
// FILE MARKER - GETTERS / SETTERS ///////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

get method_name(): MethodOf<OriginalObject> {
return this.#method_name;
}

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PROTECTED /////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/**
* Verify that the actual calls match the expected calls.
*
* @param expectedCalls - (optional) The number of calls expected. If this is
* not specified, then this method checks that the actual calls is greater
* than one -- denoting that there was call.
* @param actualCalls - The number of actual calls.
* @param expectedCalls - The number of calls expected. If this is -1, then
* just verify that the method was called without checking how many times it
* was called.
* @param codeThatThrew - See `MethodVerificationError` constructor's
* `codeThatThrew` param.
*/
public toBeCalled(
actualCalls: number,
expectedCalls: number,
codeLocation: string,
codeThatThrew: string,
): void {
if (expectedCalls === -1) {
if (actualCalls <= 0) {
throw new MethodVerificationError(
`Method "${this.#method_name}" received incorrect number of calls.`,
codeLocation,
codeThatThrew,
`Expected calls -> 1 (or more)`,
`Actual calls -> 0`,
);
Expand All @@ -48,20 +66,25 @@ export class MethodVerifier<OriginalObject> {
if (actualCalls !== expectedCalls) {
throw new MethodVerificationError(
`Method "${this.#method_name}" received incorrect number of calls.`,
codeLocation,
codeThatThrew,
`Expected calls -> ${expectedCalls}`,
`Actual calls -> ${actualCalls}`,
);
}
}

/**
* Verify that the actual arguments match the expected arguments.
* Verify that this method was called with the given args.
*
* @param actualArgs - The actual args that this method was called with.
* @param expectedArgs - The args this method is expected to have received.
* @param codeThatThrew - See `MethodVerificationError` constructor's
* `codeThatThrew` param.
*/
public toBeCalledWithArgs(
actualArgs: unknown[],
expectedArgs: unknown[],
codeLocation: string,
codeThatThrew: string,
): void {
const expectedArgsAsString = JSON.stringify(expectedArgs)
.slice(1, -1)
Expand All @@ -73,7 +96,7 @@ export class MethodVerifier<OriginalObject> {
if (expectedArgs.length != actualArgs.length) {
throw new MethodVerificationError(
`Method "${this.#method_name}" received incorrect number of arguments.`,
codeLocation,
codeThatThrew,
`Expected args -> [${expectedArgsAsString}]`,
`Actual args -> [${actualArgsAsString}]`,
);
Expand Down Expand Up @@ -109,9 +132,17 @@ export class MethodVerifier<OriginalObject> {
});
}

/**
* Verify that this method was called without arguments.
*
* @param actualArgs - The actual args that this method was called with. This
* method expects it to be an empty array.
* @param codeThatThrew - See `MethodVerificationError` constructor's
* `codeThatThrew` param.
*/
public toBeCalledWithoutArgs(
actualArgs: unknown[],
codeLocation: string,
codeThatThrew: string,
): void {
const actualArgsAsString = JSON.stringify(actualArgs)
.slice(1, -1)
Expand All @@ -120,29 +151,27 @@ export class MethodVerifier<OriginalObject> {
if (actualArgs.length > 0) {
throw new MethodVerificationError(
`Method "${this.#method_name}" received incorrect number of arguments.`,
codeLocation,
codeThatThrew,
`Expected args -> none`,
`Actual args -> [${actualArgsAsString}]`,
);
}
}

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/**
* Get the arg type in string format for the given arg.
* Check that the given arrays are exactly equal.
*
* @param arg - The arg to evaluate.
* @param a - The first array.
* @param b - The second array (which should match the first array).
*
* @returns The arg type surrounded by brackets (e.g., <type>).
* @returns True if the arrays match, false if not.
*/
#getArgType(arg: unknown): string {
if (arg && typeof arg === "object") {
if ("prototype" in arg) {
return "<" + Object.getPrototypeOf(arg) + ">";
}
return "<object>";
}

return "<" + typeof arg + ">";
#compareArrays(a: unknown[], b: unknown[]): boolean {
return a.length === b.length && a.every((val, index) => val === b[index]);
}

/**
Expand All @@ -158,14 +187,20 @@ export class MethodVerifier<OriginalObject> {
}

/**
* Check that the given arrays are exactly equal.
* Get the arg type in string format for the given arg.
*
* @param a - The first array.
* @param b - The second array (which should match the first array).
* @param arg - The arg to evaluate.
*
* @returns True if the arrays match, false if not.
* @returns The arg type surrounded by brackets (e.g., <type>).
*/
#compareArrays(a: unknown[], b: unknown[]): boolean {
return a.length === b.length && a.every((val, index) => val === b[index]);
#getArgType(arg: unknown): string {
if (arg && typeof arg === "object") {
if ("prototype" in arg) {
return "<" + Object.getPrototypeOf(arg) + ">";
}
return "<object>";
}

return "<" + typeof arg + ">";
}
}
Loading

0 comments on commit b9d7beb

Please sign in to comment.