diff --git a/mod.ts b/mod.ts index cbea2731..5bd9b7c2 100644 --- a/mod.ts +++ b/mod.ts @@ -75,78 +75,67 @@ export function Mock(constructorFn: Constructor): MockBuilder { // 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( // 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( - constructorFn: Constructor -): Interfaces.ISpy & OriginalClass; +export function Spy( + obj: OriginalObject, + dataMember: MethodOf, + 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( - obj: OriginalObject, - dataMember: MethodOf, - returnValue?: ReturnValue -): Interfaces.ISpyStub; +export function Spy( + constructorFn: Constructor, +): Interfaces.ISpy & 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( 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. @@ -167,20 +156,16 @@ export function Spy( // 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) - .returnValue(arg3 as ReturnValue) - .createForObjectMethod(); - } - - throw new Error(`Incorrect use of Spy().`); + return new SpyStubBuilder(obj as OriginalObject) + .method(methodOrReturnValue as MethodOf) + .returnValue(returnValue as ReturnValue) + .createForObjectMethod(); } //////////////////////////////////////////////////////////////////////////////// @@ -236,7 +221,7 @@ export function Stub( // 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 diff --git a/src/errors.ts b/src/errors.ts index b987bce4..1cd16fff 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -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; @@ -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; @@ -63,7 +81,9 @@ export class MethodVerificationError extends RhumError { const ignoredLines = [ "", "deno:runtime", + "callable_verifier.ts", "method_verifier.ts", + "function_expression_verifier.ts", "_mixin.ts", ".toBeCalled", ".toBeCalledWithArgs", @@ -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); } @@ -111,9 +136,9 @@ 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 @@ -121,21 +146,3 @@ export class MethodVerificationError extends RhumError { 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); - } -} diff --git a/src/fake/fake_builder.ts b/src/fake/fake_builder.ts index 24125710..ae28c612 100644 --- a/src/fake/fake_builder.ts +++ b/src/fake/fake_builder.ts @@ -17,7 +17,7 @@ export class FakeBuilder extends TestDoubleBuilder { /** * 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 { const original = new this.constructor_fn(...this.constructor_args); diff --git a/src/interfaces.ts b/src/interfaces.ts index a810bb1d..f883fb62 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,64 +1,144 @@ import type { MethodOf, MockMethodCalls } from "./types.ts"; -export interface IMethodExpectation { - toBeCalled(expectedCalls: number): void; - // toBeCalledWithArgs(...args: unknown[]): this; +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IERROR //////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Interface that all errors must follow. This is useful when a client wants to + * throw a custom error class via `.willThrow()` for mocks and fakes. + */ +export interface IError { + /** + * The name of the error (shown before the error message when thrown). + * Example: `ErrorName: `. + */ + name: string; + + /** + * The error message. + */ + message?: string; } -export interface IMethodVerification { +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IFAKE ///////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +export interface IFake { + /** + * Helper property to show that this object is a fake. + */ + is_fake: boolean; + /** - * Verify that this method was called. Optionally, verify that it was called a - * specific number of times. + * Access the method shortener to make the given method take a shortcut. * - * @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. + * @param method - The name of the method to shorten. + */ + method( + method: MethodOf, + ): IMethodChanger; +} + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IFUNCTIONEXPRESSIONVERIFIER /////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Interface of verifier that verifies function expression calls. + */ +export interface IFunctionExpressionVerifier extends IVerifier { + /** + * The args used when called. + */ + args: unknown[]; + + /** + * The number of calls made. + */ + calls: number; +} + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IMETHODEXPECTATION //////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +export interface IMethodExpectation { + /** + * Set an expectation that the given number of expected calls should be made. + * + * @param expectedCalls - (Optional) The number of expected calls. If not + * specified, then verify that there was at least one call. * * @returns `this` To allow method chaining. */ toBeCalled(expectedCalls?: number): this; - toBeCalledWithArgs(firstArg: unknown, ...restOfArgs: unknown[]): this; + + /** + * Set an expectation that the given args should be used during a method call. + * + * @param requiredArg - Used to make this method require at least one arg. + * @param restOfArgs - Any other args to check during verification. + * + * @returns `this` To allow method chaining. + */ + toBeCalledWithArgs(requiredArg: unknown, ...restOfArgs: unknown[]): this; + + /** + * Set an expectation that a method call should be called without args. + * + * @returns `this` To allow method chaining. + */ toBeCalledWithoutArgs(): this; } -export interface IPreProgrammedMethod { - willReturn(returnValue: ReturnValue): void; - willThrow(error: IError): void; -} +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IMETHODCHANGER //////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// -export interface IError { +export interface IMethodChanger { /** - * The name of the error (shown before the error message when thrown). - * Example: `ErrorName: `. + * Make the given method return the given `returnValue`. + * + * @param returnValue - The value to make the method return. */ - name: string; + willReturn(returnValue: T): void; /** - * The error message. + * Make the given method throw the given error. + * + * @param error - An error which extends the `Error` class or has the same + * interface as the `Error` class. */ - message?: string; + willThrow(error: IError & T): void; } -export interface IFake { +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IMETHODVERIFIER /////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Interface of verifier that verifies method calls. + */ +export interface IMethodVerifier extends IVerifier { /** - * Helper property to show that this object is a fake. + * Property to hold the args used when the method using this verifier was + * called. */ - is_fake: boolean; + args: unknown[]; /** - * Entry point to shortcut a method. Example: - * - * ```ts - * fake.method("methodName").willReturn(...); - * fake.method("methodName").willThrow(...); - * ``` + * Property to hold the number of times the method using this verifier was + * called. */ - method( - methodName: MethodOf, - ): IPreProgrammedMethod; + calls: number; } +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IMOCK ///////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + export interface IMock { /** * Property to track method calls. @@ -71,71 +151,93 @@ export interface IMock { is_mock: boolean; /** - * Entry point to set an expectation on a method. Example: + * Access the method expectation creator to create an expectation for the + * given method. * - * ```ts - * mock.expects("doSomething").toBeCalled(1); // Expect to call it once - * mock.doSomething(); // Call it once - * mock.verifyExpectations(); // Verify doSomething() was called once - * ``` + * @param method - The name of the method to create an expectation for. */ expects( method: MethodOf, ): IMethodExpectation; /** - * Entry point to pre-program a method. Example: + * Access the method pre-programmer to change the behavior of the given method. * - * ```ts - * mock.method("methodName").willReturn(someValue); - * mock.method("methodName").willThrow(new Error("Nope.")); - * ``` + * @param method - The name of the method to pre-program. */ - method( - methodName: MethodOf, - ): IPreProgrammedMethod; + method( + method: MethodOf, + ): IMethodChanger; /** - * Call this method after setting expectations on a method. Example: - * - * ```ts - * mock.expects("doSomething").toBeCalled(1); // Expect to call it once - * mock.doSomething(); // Call it once - * mock.verifyExpectations(); // Verify doSomething() was called once - * ``` + * Verify that all expectations from the `.expects()` calls. */ verifyExpectations(): void; } +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - ISPY ////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + export interface ISpy { + /** + * Helper property to see that this is a spy object and not the original. + */ is_spy: boolean; - stubbed_methods: Record, ISpyStub>; + /** + * Property to track all stubbed methods. This property is used when calling + * `.verify("someMethod")`. The `.verify("someMethod")` call will return the + * `ISpyStubMethod` object via `stubbed_methods["someMethod"]`. + */ + stubbed_methods: Record, ISpyStubMethod>; + + /** + * Access the method verifier. + * + * @returns A verifier to verify method calls. + */ verify( - methodName: MethodOf, - ): IMethodVerification; + method: MethodOf, + ): IMethodVerifier; } -export interface ISpyStub { +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - ISPYSTUBFUNCTIONEXPRESSION //////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Interface for spies on function expressions. + */ +export interface ISpyStubFunctionExpression { /** - * Access the method verifier in order to call verification methods like `.toBeCalled()`. Example: + * Access the function expression verifier. * - * @example - * ```ts - * // Spying on an object's method - * const spy = Spy(obj, "someMethod"); - * obj.someMethod(); - * spy.verify().toBeCalled(); + * @returns A verifier to verify function expression calls. + */ + verify(): IFunctionExpressionVerifier; +} + +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - ISPYSTUBMETHOD //////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Interface for spies on object methods. + */ +export interface ISpyStubMethod { + /** + * Access the method verifier. * - * // Spy on a function - * const spy = Spy(someFunction); - * someFunction(); - * spy.verify().toBeCalled(); - * ``` + * @returns A verifier to verify method calls. */ - verify(): IMethodVerification; + verify(): IMethodVerifier; } +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - ITESTDOUBLE /////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + export interface ITestDouble { init( original: OriginalObject, @@ -143,6 +245,40 @@ export interface ITestDouble { ): void; } -export interface ISpyStubFunctionExpression { - verify(): IMethodVerification; +//////////////////////////////////////////////////////////////////////////////// +// FILE MARKER - IVERIFIER ///////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// + +/** + * Base interface for verifiers. + */ +export interface IVerifier { + /** + * Verify that calls were made. + * + * @param expectedCalls - (Optional) The number of expected calls. If not + * specified, then verify that there was at least one call. + * + * @returns `this` To allow method chaining. + */ + toBeCalled(expectedCalls?: number): this; + + /** + * Verify that the given args were used. Takes a rest parameter of args to use + * during verification. At least one arg is required to use this method, which + * is the `requiredArg` param. + * + * @param requiredArg - Used to make this method require at least one arg. + * @param restOfArgs - Any other args to check during verification. + * + * @returns `this` To allow method chaining. + */ + toBeCalledWithArgs(requiredArg: unknown, ...restOfArgs: unknown[]): this; + + /** + * Verify that no args were used. + * + * @returns `this` To allow method chaining. + */ + toBeCalledWithoutArgs(): this; } diff --git a/src/mock/mock_builder.ts b/src/mock/mock_builder.ts index 493d9e4a..89266b1d 100644 --- a/src/mock/mock_builder.ts +++ b/src/mock/mock_builder.ts @@ -19,7 +19,7 @@ export class MockBuilder extends TestDoubleBuilder { /** * Create the mock object. * - * @returns The original object with capabilities from the Mock class. + * @returns The original object with mocking capabilities. */ public create(): ClassToMock & IMock { const original = new this.constructor_fn(...this.constructor_args); @@ -99,6 +99,7 @@ export class MockBuilder extends TestDoubleBuilder { value: (...args: unknown[]) => { // Track that this method was called mock.calls[method]++; + // TODO: copy spy approach because we need mock.expected_args // Make sure the method calls its original self const methodToCall = diff --git a/src/mock/mock_mixin.ts b/src/mock/mock_mixin.ts index afafefed..db17cfa5 100644 --- a/src/mock/mock_mixin.ts +++ b/src/mock/mock_mixin.ts @@ -14,6 +14,16 @@ class MethodExpectation { */ #expected_calls?: number | undefined; + /** + * Property to hold the expected args this method should use. + */ + #expected_args?: unknown[] | undefined; + + /** + * Property to hold the args this method was called with. + */ + #args?: unknown[] | undefined; + /** * See `MethodVerifier#method_name`. */ @@ -29,7 +39,7 @@ class MethodExpectation { ////////////////////////////////////////////////////////////////////////////// /** - * @param methodName See `MethodVerifier#method_name`. + * @param methodName - See `MethodVerifier#method_name`. */ constructor(methodName: MethodOf) { this.#method_name = methodName; @@ -49,18 +59,28 @@ class MethodExpectation { ////////////////////////////////////////////////////////////////////////////// /** - * Set an expected number of calls. - * - * @param expectedCalls - (Optional) The number of calls to receive. Defaults - * to -1 to tell `MethodVerifier` to "just check that the method was called". + * See `IMethodExpectation.toBeCalled()`. */ - public toBeCalled(expectedCalls?: number): void { + public toBeCalled(expectedCalls?: number): this { this.#expected_calls = expectedCalls; + return this; + } + + /** + * See `IMethodExpectation.toBeCalledWithArgs()`. + */ + public toBeCalledWithArgs(...expectedArgs: unknown[]): this { + this.#expected_args = expectedArgs; + return this; } - // public verify(actualCalls: number, actualArgs: unknown[]): void { - // this.verifyCalls( - // } + /** + * See `IMethodExpectation.toBeCalledWithoutArgs()`. + */ + public toBeCalledWithoutArgs(): this { + this.#expected_args = undefined; + return this; + } /** * Verify all expected calls were made. @@ -68,10 +88,8 @@ class MethodExpectation { * @param actualCalls - The number of actual calls. */ public verifyCalls(actualCalls: number): void { - this.#verifier.toBeCalled( - actualCalls, - this.#expected_calls, - ); + this.#verifier.toBeCalled(actualCalls, this.#expected_calls); + this.#verifier.toBeCalledWithoutArgs(this.#args ?? []); } } @@ -132,10 +150,7 @@ export function createMock( } /** - * Create a method expectation, which is basically asserting calls. - * - * @param method - The method to create an expectation for. - * @returns A method expectation. + * See `IMock.expects()`. */ public expects( method: MethodOf, @@ -146,10 +161,7 @@ export function createMock( } /** - * 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. + * See `IMock.method()`. */ public method( methodName: MethodOf, @@ -176,7 +188,7 @@ export function createMock( } /** - * Verify all expectations created in this mock. + * See `IMock.verifyExpectations()`. */ public verifyExpectations(): void { this.#expectations.forEach((e: MethodExpectation) => { @@ -191,8 +203,10 @@ export function createMock( /** * 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. */ diff --git a/src/spy/spy_builder.ts b/src/spy/spy_builder.ts index 8421364a..9b6b7d4c 100644 --- a/src/spy/spy_builder.ts +++ b/src/spy/spy_builder.ts @@ -8,11 +8,11 @@ import { TestDoubleBuilder } from "../test_double_builder.ts"; * create a spy object. Its `create()` method returns an instance of `Spy`, * which is basically an original object with stubbed data members. * - * This builder differs from the `SpyStub` because it stubs out the - * entire class, whereas the `SpyStub` stubs specific data members. + * This builder differs from the `SpyStub` because it stubs out the entire + * class, whereas the `SpyStub` stubs specific data members. * - * Under the hood, this builder uses `SpyStub` to stub the data members - * in the class. + * Under the hood, this builder uses `SpyStub` to stub the data members in the + * class. */ export class SpyBuilder extends TestDoubleBuilder { ////////////////////////////////////////////////////////////////////////////// diff --git a/src/spy/spy_mixin.ts b/src/spy/spy_mixin.ts index cafdc4d9..9ca03031 100644 --- a/src/spy/spy_mixin.ts +++ b/src/spy/spy_mixin.ts @@ -1,5 +1,5 @@ import type { Constructor, MethodOf } from "../types.ts"; -import type { IMethodVerification, ISpy, ISpyStub } from "../interfaces.ts"; +import type { IMethodVerifier, ISpy, ISpyStubMethod } from "../interfaces.ts"; import { SpyStubBuilder } from "./spy_stub_builder.ts"; /** @@ -14,16 +14,17 @@ export function createSpy( >; return new class SpyExtension extends Original { /** - * Helper property to see that this is a mock object and not the original. + * See `ISpy#is_spy`. */ is_spy = true; /** - * Property of stubbed methods. Each stubbed method has tracking (e.g., `spy.verify("someMethod").toBeCalled()`. + * See `ISpy#stubbed_methods`. */ - #stubbed_methods!: Record, ISpyStub>; + #stubbed_methods!: Record, ISpyStubMethod>; + /** - * The original object that this class creates a mock of. + * The original object that this class creates a spy out of. */ #original!: OriginalObject; @@ -31,7 +32,7 @@ export function createSpy( // FILE MARKER - GETTERS / SETTERS ///////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// - get stubbed_methods(): Record, ISpyStub> { + get stubbed_methods(): Record, ISpyStubMethod> { return this.#stubbed_methods; } @@ -40,7 +41,7 @@ export function createSpy( //////////////////////////////////////////////////////////////////////////// /** - * @param original - The original object to mock. + * @param original - The original object to create a spy out of. * @param methodsToTrack - The original object's method to make trackable. */ public init(original: OriginalObject, methodsToTrack: string[]) { @@ -51,11 +52,11 @@ export function createSpy( } /** - * Get the verifier for the given method to do actual verification using + * Get the verifier for the given method to do actual verification. */ public verify( methodName: MethodOf, - ): IMethodVerification { + ): IMethodVerifier { return this.#stubbed_methods[methodName].verify(); } @@ -66,16 +67,18 @@ export function createSpy( /** * 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. */ #constructStubbedMethodsProperty( methodsToTrack: string[], - ): Record, ISpyStub> { + ): Record, ISpyStubMethod> { const stubbedMethods: Partial< - Record, ISpyStub> + Record, ISpyStubMethod> > = {}; methodsToTrack.forEach((method: string) => { @@ -86,7 +89,10 @@ export function createSpy( stubbedMethods[method as MethodOf] = spyMethod; }); - return stubbedMethods as Record, ISpyStub>; + return stubbedMethods as Record< + MethodOf, + ISpyStubMethod + >; } }(); } diff --git a/src/spy/spy_stub_builder.ts b/src/spy/spy_stub_builder.ts index 1dfc9277..745aeb8e 100644 --- a/src/spy/spy_stub_builder.ts +++ b/src/spy/spy_stub_builder.ts @@ -1,20 +1,38 @@ import type { MethodOf } from "../types.ts"; -import type { IMethodVerification, ISpyStub } from "../interfaces.ts"; +import type { + IFunctionExpressionVerifier, + IMethodVerifier, + ISpyStubFunctionExpression, + ISpyStubMethod, +} from "../interfaces.ts"; import { MethodVerifier } from "../verifiers/method_verifier.ts"; import { FunctionExpressionVerifier } from "../verifiers/function_expression_verifier.ts"; +/** + * This class helps verifying function expression calls. It's a wrapper for + * `FunctionExpressionVerifier` only to make the verification methods (e.g., + * `toBeCalled()`) have a shorter syntax -- allowing the user of this library to + * only pass in expected calls as opposed to expected calls and actual calls. + */ class SpyStubFunctionExpressionVerifier extends FunctionExpressionVerifier { - #calls: number; + /** + * See `IFunctionExpressionVerifier.args`. + */ #args: unknown[]; + /** + * See `IFunctionExpressionVerifier.calls`. + */ + #calls: number; + ////////////////////////////////////////////////////////////////////////////// // FILE MARKER - CONSTRUCTOR ///////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /** - * @param name - The name of the function using this verifier. - * @param calls - See `this.#calls`. - * @param args - See `this.#args`. + * @param name - See `FunctionExpressionVerifier.#name`. + * @param calls - See `IFunctionExpressionVerifier.calls`. + * @param args - See `IFunctionExpressionVerifier.args`. */ constructor( name: string, @@ -26,6 +44,25 @@ class SpyStubFunctionExpressionVerifier extends FunctionExpressionVerifier { this.#args = args; } + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - GETTERS / SETTERS ///////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + get args(): unknown[] { + return this.#args; + } + + get calls(): number { + return this.#calls; + } + + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * See `IVerifier.toBeCalled()`. + */ public toBeCalled(expectedCalls?: number): this { return super.toBeCalled( this.#calls, @@ -33,37 +70,42 @@ class SpyStubFunctionExpressionVerifier extends FunctionExpressionVerifier { ); } + /** + * See `IVerifier.toBeCalledWithArgs()`. + */ public toBeCalledWithArgs(...expectedArgs: unknown[]): this { return super.toBeCalledWithArgs( this.#args, expectedArgs, ); } + + /** + * See `IVerifier.toBeCalledWithoutArgs()`. + */ public toBeCalledWithoutArgs() { super.toBeCalledWithoutArgs( this.#args, - `.verify().toBeCalledWithoutArgs()`, ); return this; } } /** - * The `SpyStub` class' verifier. It extends the `MethodVerifier` just to use - * its verification methods. In order to properly show stack traces in the - * context of the `SpyStub`, this verifier is used to provide the - * `codeThatThrew` argument to the `MethodVerifier` class' methods. + * This class helps verifying object method calls. It's a wrapper for + * `MethodVerifier` only to make the verification methods (e.g., `toBeCalled()`) + * have a shorter syntax -- allowing the user of this library to only pass in + * expected calls as opposed to expected calls and actual calls. */ -class SpyStubMethodVerifier< - OriginalObject, -> extends MethodVerifier { +class SpyStubMethodVerifier + extends MethodVerifier { /** - * Property to hold the arguments this method was called with. + * See `IMethodVerifier.args`. */ #args: unknown[]; /** - * Property to hold the number of time this method was called. + * See `IMethodVerifier.calls`. */ #calls: number; @@ -72,9 +114,9 @@ class SpyStubMethodVerifier< ////////////////////////////////////////////////////////////////////////////// /** - * @param methodName - See `MethodVerifier#method_name`. - * @param calls - See `this.#calls`. - * @param args - See `this.#args`. + * @param methodName - See `MethodVerifier.method_name`. + * @param calls - See `IMethodVerifier.calls`. + * @param args - See `IMethodVerifier.args`. */ constructor( methodName: MethodOf, @@ -103,15 +145,7 @@ class SpyStubMethodVerifier< ////////////////////////////////////////////////////////////////////////////// /** - * 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. + * See `IVerifier.toBeCalled()`. */ public toBeCalled(expectedCalls?: number): this { return super.toBeCalled( @@ -121,12 +155,7 @@ class SpyStubMethodVerifier< } /** - * Verify that this method was called with the following arguments. - * - * @param expectedArgs - The expected arguments that this method should be - * called with. - * - * @returns this To allow method chaining. + * See `IVerifier.toBeCalledWithArgs()`. */ public toBeCalledWithArgs(...expectedArgs: unknown[]): this { return super.toBeCalledWithArgs( @@ -136,9 +165,7 @@ class SpyStubMethodVerifier< } /** - * Verify that this method was called without arguments. - * - * @returns this To allow method chaining. + * See `IVerifier.toBeCalledWithoutArgs()`. */ public toBeCalledWithoutArgs(): this { return super.toBeCalledWithoutArgs( @@ -160,7 +187,7 @@ export class SpyStubBuilder { #calls = 0; /** - * Property to hold the arguments this method was last called with. + * Property to hold the args this method was last called with. */ #last_called_with_args: unknown[] = []; @@ -194,17 +221,42 @@ export class SpyStubBuilder { this.#original = original; } + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + + /** + * Set the name of the method this spy stub is stubbing. + * + * @param method - The name of the method. This must be a method of the + * original object. + * + * @returns `this` To allow method chaining. + */ public method(method: MethodOf): this { this.#method = method; return this; } + /** + * Set the return value of this spy stub. + * + * @param returnValue - The value to return when this spy stub is called. + * + * @returns `this` To allow method chaining. + */ public returnValue(returnValue: ReturnValue): this { this.#return_value = returnValue; return this; } - public createForObjectMethod(): ISpyStub { + /** + * Create this spy stub for an object's method. + * + * @returns `this` behind the `ISpyStubMethod` interface so that only + * `ISpyStubMethod` data members can be seen/called. + */ + public createForObjectMethod(): ISpyStubMethod { this.#stubOriginalMethodWithTracking(); Object.defineProperty(this, "verify", { @@ -216,22 +268,29 @@ export class SpyStubBuilder { ), }); - return this as unknown as ISpyStub & { verify: IMethodVerification }; + return this as unknown as ISpyStubMethod & { verify: IMethodVerifier }; } - public createForFunctionExpression(): ISpyStub { + /** + * Create this spy stub for a function expression. + * + * @returns `this` behind the `ISpyStubFunctionExpression` interface so that + * only `ISpyStubFunctionExpression` data members can be seen/called. + */ + public createForFunctionExpression(): ISpyStubFunctionExpression { const ret = (...args: unknown[]) => { this.#calls++; this.#last_called_with_args = args; return this.#return_value ?? "spy-stubbed"; }; - ret.verify = () => + ret.verify = (): IFunctionExpressionVerifier => new SpyStubFunctionExpressionVerifier( (this.#original as unknown as { name: string }).name, this.#calls, this.#last_called_with_args, ); + return ret; } diff --git a/src/test_double_builder.ts b/src/test_double_builder.ts index b204a342..b20cac96 100644 --- a/src/test_double_builder.ts +++ b/src/test_double_builder.ts @@ -11,8 +11,8 @@ export class TestDoubleBuilder { protected constructor_fn: Constructor; /** - * A list of arguments the class constructor takes. This is used to - * instantiate the original with arguments (if needed). + * A list of args the class constructor takes. This is used to instantiate the + * original with args (if needed). */ protected constructor_args: unknown[] = []; @@ -43,7 +43,7 @@ export class TestDoubleBuilder { /** * Construct an object of this class. * - * @param constructorFn - See this#constructor_fn. + * @param constructorFn - See `this.constructor_fn`. */ constructor(constructorFn: Constructor) { this.constructor_fn = constructorFn; @@ -57,7 +57,7 @@ export class TestDoubleBuilder { * 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 + * @param args - A rest parameter of args 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. diff --git a/src/types.ts b/src/types.ts index 9ae6c4a9..ce313ce5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,15 +13,6 @@ export type Constructor = new (...args: any[]) => T; * * This is a record where the key is the method that was called and the value is * the number of times the method was called. - * - * @example - * ```ts - * { - * someMethod: 0, - * someOtherMethod: 3, - * someOtherOtherMethod: 2, - * } - * ``` */ export type MockMethodCalls = Record; @@ -33,22 +24,6 @@ export type MockMethodCalls = Record; * Describes the type as a method of the given generic `Object`. * * This is used for type-hinting in places like `.verify("someMethod")`. - * - * @example - * ```ts - * class Hello { - * some_property = true; - * someMethod(): boolean { return true;} - * } - * - * function callMethod(method: MethodOf) { ... } - * - * callMethod("someMethod") // Ok - * - * // Shows the following error: Argument of type '"some_property"' is not - * // assignable to parameter of type 'MethodOf' - * callMethod("some_property") - * ``` */ export type MethodOf = { // deno-lint-ignore no-explicit-any diff --git a/src/verifiers/callable_verifier.ts b/src/verifiers/callable_verifier.ts index 83a97cab..d1950a69 100644 --- a/src/verifiers/callable_verifier.ts +++ b/src/verifiers/callable_verifier.ts @@ -1,13 +1,21 @@ -import { MethodVerificationError } from "../errors.ts"; +import { VerificationError } from "../errors.ts"; export class CallableVerifier { + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** - * Make a user friendly version of the expected args. This will be displayed - * in the `MethodVerificationError` stack trace. For example: + * Make a user friendly version of the args. This is for display in the + * `VerificationError` stack trace. For example, the original args ... + * + * [true, false, "hello"] * - * [true, false, "hello"] -> true, false, "hello" + * ... becomes ... * - * The above would result in the following stack trace message: + * true, false, "hello" + * + * The above will ultimately end up in stack trace messages like: * * .toBeCalledWith(true, false, "hello") * @@ -16,13 +24,30 @@ export class CallableVerifier { * "hello" string has its quotes missing: * * .toBeCalledWith([true, false, hello]) + * + * @param args - The args to convert to a string. + * + * @returns The args as a string. */ - protected argsAsString(expectedArgs: unknown[]): string { - return JSON.stringify(expectedArgs) + protected argsAsString(args: unknown[]): string { + return JSON.stringify(args) .slice(1, -1) .replace(/,/g, ", "); } + /** + * Same as `this.argsAsString()`, but add typings to the args. For example: + * + * [true, false, "hello"] + * + * ... becomes ... + * + * true, false, "hello" + * + * @param args - The args to convert to a string. + * + * @returns The args as a string with typings. + */ protected argsAsStringWithTypes(args: unknown[]): string { return args.map((arg: unknown) => { return `${JSON.stringify(arg)}${this.getArgType(arg)}`; @@ -47,42 +72,69 @@ export class CallableVerifier { return "<" + typeof arg + ">"; } + /** + * Verify that the number of actual calls matches the number of expected + * calls. + * + * @param actualCalls - The actual number of calls. + * @param expectedCalls - The expected number of calls. + * @param errorMessage - The error message to show in the stack trace. + * @param codeThatThrew - The code using this verification. + * + * @returns `this` To allow method chaining. + */ protected verifyToBeCalled( actualCalls: number, expectedCalls: number | undefined, errorMessage: string, codeThatThrew: string, ): void { + // If expected calls were not specified, then just check that the method was + // called at least once if (!expectedCalls) { - if (actualCalls <= 0) { - throw new MethodVerificationError( - errorMessage, - codeThatThrew, - `Expected calls -> 1 (or more)`, - `Actual calls -> 0`, - ); + if (actualCalls > 0) { + return; } - return; - } - if (actualCalls !== expectedCalls) { - throw new MethodVerificationError( + throw new VerificationError( errorMessage, codeThatThrew, - `Expected calls -> ${expectedCalls}`, - `Actual calls -> ${actualCalls}`, + `Expected calls -> 1 (or more)`, + `Actual calls -> 0`, ); } + + // If we get here, then we gucci. No need to process further. + if (actualCalls === expectedCalls) { + return; + } + + // If we get here, then the actual number of calls do not match the expected + // number of calls, so we should throw an error + throw new VerificationError( + errorMessage, + codeThatThrew, + `Expected calls -> ${expectedCalls}`, + `Actual calls -> ${actualCalls}`, + ); } - protected verifyToBeCalledWithArgsTooManyArguments( + /** + * Verify that the number of expected args is not more than the actual args. + * + * @param actualArgs - The actual args. + * @param expectedArgs - The expected args. + * @param errorMessage - The error message to show in the stack trace. + * @param codeThatThrew - The code using this verification. + */ + protected verifyToBeCalledWithArgsTooManyArgs( actualArgs: unknown[], expectedArgs: unknown[], errorMessage: string, codeThatThrew: string, ): void { if (expectedArgs.length > actualArgs.length) { - throw new MethodVerificationError( + throw new VerificationError( errorMessage, codeThatThrew, `Expected args -> ${this.argsAsStringWithTypes(expectedArgs)}`, @@ -95,14 +147,22 @@ export class CallableVerifier { } } - protected verifyToBeCalledWithArgsTooFewArguments( + /** + * Verify that the number of expected args is not less than the actual args. + * + * @param actualArgs - The actual args. + * @param expectedArgs - The expected args. + * @param errorMessage - The error message to show in the stack trace. + * @param codeThatThrew - The code using this verification. + */ + protected verifyToBeCalledWithArgsTooFewArgs( actualArgs: unknown[], expectedArgs: unknown[], errorMessage: string, codeThatThrew: string, ): void { if (expectedArgs.length < actualArgs.length) { - throw new MethodVerificationError( + throw new VerificationError( errorMessage, codeThatThrew, `Expected call -> (${this.argsAsStringWithTypes(expectedArgs)})`, @@ -111,6 +171,14 @@ export class CallableVerifier { } } + /** + * Verify that the expected args match the actual args by value and type. + * + * @param actualArgs - The actual args. + * @param expectedArgs - The expected args. + * @param errorMessage - The error message to show in the stack trace. + * @param codeThatThrew - The code using this verification. + */ protected verifyToBeCalledWithArgsUnexpectedValues( actualArgs: unknown[], expectedArgs: unknown[], @@ -119,50 +187,74 @@ export class CallableVerifier { ): void { expectedArgs.forEach((arg: unknown, index: number) => { const parameterPosition = index + 1; - if (actualArgs[index] !== arg) { - if (this.#comparingArrays(actualArgs[index], arg)) { - const match = this.#compareArrays( - actualArgs[index] as unknown[], - arg as unknown[], - ); - if (match) { - return; - } - } - const unexpectedArg = `\`${arg}${this.getArgType(arg)}\``; + // Args match? We gucci. + if (actualArgs[index] === arg) { + return; + } - throw new MethodVerificationError( - errorMessage - .replace("{{ unexpected_arg }}", unexpectedArg) - .replace("{{ parameter_position }}", parameterPosition.toString()), - codeThatThrew, - `Expected call -> (${this.argsAsStringWithTypes(expectedArgs)})`, - `Actual call -> (${this.argsAsStringWithTypes(actualArgs)})`, + // Args do not match? Check if we are comparing arrays and see if they + // match + if (this.#comparingArrays(actualArgs[index], arg)) { + const match = this.#compareArrays( + actualArgs[index] as unknown[], + arg as unknown[], ); + // Arrays match? We gucci. + if (match) { + return; + } } + + // Alright, we have an unexpected arg, so throw an error + const unexpectedArg = `\`${arg}${this.getArgType(arg)}\``; + + throw new VerificationError( + errorMessage + .replace("{{ unexpected_arg }}", unexpectedArg) + .replace("{{ parameter_position }}", parameterPosition.toString()), + codeThatThrew, + `Expected call -> (${this.argsAsStringWithTypes(expectedArgs)})`, + `Actual call -> (${this.argsAsStringWithTypes(actualArgs)})`, + ); }); } + /** + * Verify that the no args were used. + * + * @param actualArgs - The actual args (if any). + * @param errorMessage - The error message to show in the stack trace. + * @param codeThatThrew - The code using this verification. + */ protected verifyToBeCalledWithoutArgs( actualArgs: unknown[], errorMessage: string, codeThatThrew: string, - ): void { + ): this { const actualArgsAsString = JSON.stringify(actualArgs) .slice(1, -1) .replace(/,/g, ", "); - if (actualArgs.length > 0) { - throw new MethodVerificationError( - errorMessage, - codeThatThrew, - `Expected args -> (no args)`, - `Actual args -> (${actualArgsAsString})`, - ); + if (actualArgs.length === 0) { + return this; } + + // One arg? Say "arg". More than one arg? Say "args". Yaaaaaarg. + const argNoun = actualArgs.length > 1 ? "args" : "arg"; + + throw new VerificationError( + errorMessage, + codeThatThrew.replace("{{ arg_noun }}", argNoun), + `Expected args -> (no args)`, + `Actual args -> (${actualArgsAsString})`, + ); } + ////////////////////////////////////////////////////////////////////////////// + // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** * Check that the given arrays are exactly equal. * diff --git a/src/verifiers/function_expression_verifier.ts b/src/verifiers/function_expression_verifier.ts index eb961095..74469359 100644 --- a/src/verifiers/function_expression_verifier.ts +++ b/src/verifiers/function_expression_verifier.ts @@ -1,10 +1,9 @@ -import { MethodVerificationError } from "../errors.ts"; import { CallableVerifier } from "./callable_verifier.ts"; /** * Test doubles use this class to verify that their methods were called, were - * called with a number of arguments, were called with specific types of - * arguments, and so on. + * called with a number of args, were called with specific types of args, and so + * on. */ export class FunctionExpressionVerifier extends CallableVerifier { /** @@ -17,7 +16,7 @@ export class FunctionExpressionVerifier extends CallableVerifier { ////////////////////////////////////////////////////////////////////////////// /** - * @param name - See this#name. + * @param name - See `this.#name`. */ constructor(name: string) { super(); @@ -64,92 +63,52 @@ export class FunctionExpressionVerifier extends CallableVerifier { * * @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. */ protected toBeCalledWithArgs( actualArgs: unknown[], expectedArgs: unknown[], ): this { const expectedArgsAsString = this.argsAsString(expectedArgs); + const codeThatThrew = + `.verify().toBeCalledWithArgs(${expectedArgsAsString})`; - this.verifyToBeCalledWithArgsTooManyArguments( + this.verifyToBeCalledWithArgsTooManyArgs( actualArgs, expectedArgs, - `Function "${this.#name}" received too many arguments.`, - `.verify().toBeCalledWithArgs(${expectedArgsAsString})`, + `Function "${this.#name}" received too many args.`, + codeThatThrew, ); - this.verifyToBeCalledWithArgsTooFewArguments( + this.verifyToBeCalledWithArgsTooFewArgs( actualArgs, expectedArgs, - `Function "${this.#name}" was called with ${actualArgs.length} ${ - actualArgs.length > 1 ? "args" : "arg" - } instead of ${expectedArgs.length}.`, - `.verify().toBeCalledWithArgs(${expectedArgsAsString})`, + `Function "${this.#name}" was called with ${actualArgs.length} {{ arg_noun }} instead of ${expectedArgs.length}.`, + codeThatThrew, ); this.verifyToBeCalledWithArgsUnexpectedValues( actualArgs, expectedArgs, `Function "${this.#name}" received unexpected arg {{ unexpected_arg }} at parameter position {{ parameter_position }}.`, - `.verify().toBeCalledWithArgs(${expectedArgsAsString})`, + codeThatThrew, ); return this; } /** - * Verify that this method was called without arguments. + * Verify that this method was called without args. * * @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( + protected toBeCalledWithoutArgs( actualArgs: unknown[], - codeThatThrew: string, - ): void { - const actualArgsAsString = JSON.stringify(actualArgs) - .slice(1, -1) - .replace(/,/g, ", "); - - if (actualArgs.length > 0) { - throw new MethodVerificationError( - `Function "${this.#name}" was called with args when expected to receive no args.`, - codeThatThrew, - `Expected -> (no args)`, - `Actual -> (${actualArgsAsString})`, - ); - } - } - - ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// - - /** - * Check that the given arrays are exactly equal. - * - * @param a - The first array. - * @param b - The second array (which should match the first array). - * - * @returns True if the arrays match, false if not. - */ - #compareArrays(a: unknown[], b: unknown[]): boolean { - return a.length === b.length && a.every((val, index) => val === b[index]); - } - - /** - * Are we comparing arrays? - * - * @param obj1 - Object to evaluate if it is an array. - * @param obj2 - Object to evaluate if it is an array. - * - * @returns True if yes, false if no. - */ - #comparingArrays(obj1: unknown, obj2: unknown): boolean { - return Array.isArray(obj1) && Array.isArray(obj2); + ): this { + return super.verifyToBeCalledWithoutArgs( + actualArgs, + `Function "${this.#name}" was called with args when expected to receive no args.`, + `.verify().toBeCalledWithoutArgs()`, + ); } } diff --git a/src/verifiers/method_verifier.ts b/src/verifiers/method_verifier.ts index 29ed8f8d..a290b5ff 100644 --- a/src/verifiers/method_verifier.ts +++ b/src/verifiers/method_verifier.ts @@ -1,11 +1,10 @@ import type { MethodOf } from "../types.ts"; -import { MethodVerificationError } from "../errors.ts"; import { CallableVerifier } from "./callable_verifier.ts"; /** * Test doubles use this class to verify that their methods were called, were - * called with a number of arguments, were called with specific types of - * arguments, and so on. + * called with a number of args, were called with specific types of args, and so + * on. */ export class MethodVerifier extends CallableVerifier { /** @@ -19,7 +18,7 @@ export class MethodVerifier extends CallableVerifier { ////////////////////////////////////////////////////////////////////////////// /** - * @param methodName - See this#method_name. + * @param methodName - See `this.#method_name`. */ constructor(methodName?: MethodOf) { super(); @@ -35,7 +34,7 @@ export class MethodVerifier extends CallableVerifier { } ////////////////////////////////////////////////////////////////////////////// - // FILE MARKER - METHODS - PROTECTED ///////////////////////////////////////// + // FILE MARKER - METHODS - PUBLIC //////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// /** @@ -45,6 +44,8 @@ export class MethodVerifier extends CallableVerifier { * @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. + * + * @returns `this` To allow method chaining. */ public toBeCalled( actualCalls: number, @@ -66,44 +67,50 @@ export class MethodVerifier extends CallableVerifier { * * @param actualArgs - The actual args that this method was called with. * @param expectedArgs - The args this method is expected to have received. + * + * @returns `this` To allow method chaining. */ public toBeCalledWithArgs( actualArgs: unknown[], expectedArgs: unknown[], ): this { const expectedArgsAsString = this.argsAsString(expectedArgs); + const codeThatThrew = + `.verify("${this.#method_name}").toBeCalledWithArgs(${expectedArgsAsString})`; - this.verifyToBeCalledWithArgsTooManyArguments( + this.verifyToBeCalledWithArgsTooManyArgs( actualArgs, expectedArgs, - `Method "${this.#method_name}" received too many arguments.`, - `.verify("${this.#method_name}").toBeCalledWithArgs(${expectedArgsAsString})`, + `Method "${this.#method_name}" received too many args.`, + codeThatThrew, ); - this.verifyToBeCalledWithArgsTooFewArguments( + this.verifyToBeCalledWithArgsTooFewArgs( actualArgs, expectedArgs, `Method "${this.#method_name}" was called with ${actualArgs.length} ${ actualArgs.length > 1 ? "args" : "arg" } instead of ${expectedArgs.length}.`, - `.verify("${this.#method_name}").toBeCalledWithArgs(${expectedArgsAsString})`, + codeThatThrew, ); this.verifyToBeCalledWithArgsUnexpectedValues( actualArgs, expectedArgs, `Method "${this.#method_name}" received unexpected arg {{ unexpected_arg }} at parameter position {{ parameter_position }}.`, - `.verify("${this.#method_name}").toBeCalledWithArgs(${expectedArgsAsString})`, + codeThatThrew, ); return this; } /** - * Verify that this method was called without arguments. + * Verify that this method was called without args. * * @param actualArgs - The actual args that this method was called with. This * method expects it to be an empty array. + * + * @returns `this` To allow method chaining. */ public toBeCalledWithoutArgs( actualArgs: unknown[], diff --git a/tests/deno/unit/mod/mock_test.ts b/tests/deno/unit/mod/mock_test.ts index 1bf2c115..9f7dd9d8 100644 --- a/tests/deno/unit/mod/mock_test.ts +++ b/tests/deno/unit/mod/mock_test.ts @@ -245,7 +245,7 @@ Deno.test("Mock()", async (t) => { const mock = Mock(TestObjectThree).create(); assertEquals(mock.is_mock, true); - mock.expects("hello").toBeCalled(2); + mock.expects("hello").toBeCalled(2).toBeCalledWithoutArgs(); mock.test(); mock.verifyExpectations(); }, diff --git a/tests/deno/unit/mod/spy_test.ts b/tests/deno/unit/mod/spy_test.ts index 60d3662f..a8a54d51 100644 --- a/tests/deno/unit/mod/spy_test.ts +++ b/tests/deno/unit/mod/spy_test.ts @@ -35,7 +35,7 @@ class ResourceParameterized { // This method will be stubbed to return "spy-stubbed", so during // `spy.verify().toBeCalled()`, `this.methodThatLogs()` should not be expected // to be called. - public methodThatGets(paramString1: string, paramString2: string) { + public methodThatGets(_paramString1: string, _paramString2: string) { this.methodThatLogs("Handle GET"); return "Do GET"; } @@ -44,9 +44,9 @@ class ResourceParameterized { // `spy.verify().toBeCalled()`, `this.methodThatLogs()` should not be expected // to be called. public methodThatPosts( - paramBool1: boolean, - paramBool2: boolean, - paramArray: string[], + _paramBool1: boolean, + _paramBool2: boolean, + _paramArray: string[], ) { this.methodThatLogs("Handle POSt"); return "Do POST"; @@ -70,6 +70,7 @@ Deno.test("Spy()", async (t) => { const spy2 = Spy(Resource); const stubbedReturnValue = spy2.methodThatLogs(); // We called it, ... spy2.verify("methodThatLogs").toBeCalled(); // ... so we can verify it was called ... + assertEquals(stubbedReturnValue, "spy-stubbed"); }, );