Skip to content

Commit

Permalink
chore: hella clean up -- docblocks; DRY; early returns; abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
crookse committed Apr 26, 2022
1 parent f15ab73 commit 00551a2
Show file tree
Hide file tree
Showing 16 changed files with 637 additions and 395 deletions.
105 changes: 45 additions & 60 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,78 +75,67 @@ export function Mock<T>(constructorFn: Constructor<T>): MockBuilder<T> {
// 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<ReturnValue>(
// deno-lint-ignore no-explicit-any
fn: (...args: any[]) => ReturnValue,
returnValue?: ReturnValue
functionExpression: (...args: any[]) => ReturnValue,
returnValue?: ReturnValue,
// deno-lint-ignore no-explicit-any
): Interfaces.ISpyStubFunctionExpression & ((...args: any[]) => ReturnValue);

/**
* 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
* ```
* Create a spy out of an object's method.
*
* @param constructorFn - The constructor function to create a spy out of. This
* can be `class Something{ }` or `function Something() { }`.
* @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 Instance of `Spy`, which is an extension of the o.
* @returns The original method with spying capabilities.
*/
export function Spy<OriginalClass>(
constructorFn: Constructor<OriginalClass>
): Interfaces.ISpy<OriginalClass> & OriginalClass;
export function Spy<OriginalObject, ReturnValue>(
obj: OriginalObject,
dataMember: MethodOf<OriginalObject>,
returnValue?: ReturnValue,
): Interfaces.ISpyStubMethod;

/**
* 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
* ```
* Create spy out of a class.
*
* @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.
* @param constructorFn - The constructor function of the object to spy on.
*
* @returns A spy stub that can be verified.
* @returns The original object with spying capabilities.
*/
export function Spy<OriginalObject, ReturnValue>(
obj: OriginalObject,
dataMember: MethodOf<OriginalObject>,
returnValue?: ReturnValue
): Interfaces.ISpyStub;
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 obj - (Optional) The object receiving the stub. Defaults to a stub
* function.
* @param arg2 - (Optional) The data member on the object to be stubbed.
* Only used if `obj` is an object.
* @param arg3 - (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.
* 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 - The object 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>(
obj: unknown,
arg2?: unknown,
arg3?: unknown
methodOrReturnValue?: unknown,
returnValue?: unknown,
): unknown {
if (typeof obj === "function") {
// If the function has the prototype field, the it's a constructor function.
Expand All @@ -167,20 +156,16 @@ export function Spy<OriginalObject, ReturnValue>(
// Not that function declarations (e.g., function hello() { }) will have
// "prototype" and will go through the SpyBuilder() flow above.
return new SpyStubBuilder(obj as OriginalObject)
.returnValue(arg2 as ReturnValue)
.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.
if (arg2 !== undefined) {
return new SpyStubBuilder(obj as OriginalObject)
.method(arg2 as MethodOf<OriginalObject>)
.returnValue(arg3 as ReturnValue)
.createForObjectMethod();
}

throw new Error(`Incorrect use of Spy().`);
return new SpyStubBuilder(obj as OriginalObject)
.method(methodOrReturnValue as MethodOf<OriginalObject>)
.returnValue(returnValue as ReturnValue)
.createForObjectMethod();
}

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -236,7 +221,7 @@ export function Stub<OriginalObject, ReturnValue>(

// 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
// 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
Expand Down
55 changes: 31 additions & 24 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,30 @@ export class FakeError extends RhumError {
}
}

/**
* Error to thrown in relation to mock logic.
*/
export class MockError extends RhumError {
constructor(message: string) {
super("MockError", message);
}
}

/**
* Error to throw in relation to spy logic.
*/
export class SpyError extends RhumError {
constructor(message: string) {
super("SpyError", message);
}
}

/**
* Error to throw in relation to method verification logic. For example, when a
* method is being verified that it was called once via
* `mock.method("doSomething").toBeCalled(1)`.
*/
export class MethodVerificationError extends RhumError {
export class VerificationError extends RhumError {
#actual_results: string;
#code_that_threw: string;
#expected_results: string;
Expand All @@ -44,7 +62,7 @@ export class MethodVerificationError extends RhumError {
actualResults: string,
expectedResults: string,
) {
super("MethodVerificationError", message);
super("VerificationError", message);
this.#code_that_threw = codeThatThrew;
this.#actual_results = actualResults;
this.#expected_results = expectedResults;
Expand All @@ -63,7 +81,9 @@ export class MethodVerificationError extends RhumError {
const ignoredLines = [
"<anonymous>",
"deno:runtime",
"callable_verifier.ts",
"method_verifier.ts",
"function_expression_verifier.ts",
"_mixin.ts",
".toBeCalled",
".toBeCalledWithArgs",
Expand All @@ -87,8 +107,13 @@ export class MethodVerificationError extends RhumError {
return false;
});

// Sometimes, the error stack will contain the problematic file twice. We only care about showing the problematic file once in this "concise" stack.
// In order to check for this, we check to see if the array contains more than 2 values. The first value should be the MethodVerificationError message. The second value should be the first instance of the problematic file. Knowing this, we can slice the array to contain only the error message and the first instance of the problematic file.
// Sometimes, the error stack will contain the problematic file twice. We
// only care about showing the problematic file once in this "concise"
// stack. In order to check for this, we check to see if the array contains
// more than 2 values. The first value should be the `VerificationError`
// message. The second value should be the first instance of the problematic
// file. Knowing this, we can slice the array to contain only the error
// message and the first instance of the problematic file.
if (conciseStackArray.length > 2) {
conciseStackArray = conciseStackArray.slice(0, 2);
}
Expand All @@ -111,31 +136,13 @@ export class MethodVerificationError extends RhumError {
newStack += `\n ${this.#expected_results}`;

if (lineNumber) {
newStack += `\n\nCheck the above '${
newStack += `\n\nCheck the above "${
filename.replace("/", "")
}' file at/around line ${lineNumber} for the following code to fix this error:`;
}" file at/around line ${lineNumber} for code like the following to fix this error:`;
newStack += `\n ${this.#code_that_threw}`;
}
newStack += "\n\n\n"; // Give spacing when displayed in the console

this.stack = newStack;
}
}

/**
* Error to thrown in relation to mock logic.
*/
export class MockError extends RhumError {
constructor(message: string) {
super("MockError", message);
}
}

/**
* Error to throw in relation to spy logic.
*/
export class SpyError extends RhumError {
constructor(message: string) {
super("SpyError", message);
}
}
2 changes: 1 addition & 1 deletion src/fake/fake_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class FakeBuilder<ClassToFake> extends TestDoubleBuilder<ClassToFake> {
/**
* Create the fake object.
*
* @returns The original object with capabilities from the fake class.
* @returns The original object with faking capabilities.
*/
public create(): ClassToFake & IFake<ClassToFake> {
const original = new this.constructor_fn(...this.constructor_args);
Expand Down
Loading

0 comments on commit 00551a2

Please sign in to comment.