-
Notifications
You must be signed in to change notification settings - Fork 622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(testing): Add mocking utilities #2048
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
4380bbc
feat(testing): Add mocking utilities
KyleJune 771b352
Merge branch 'main' into kj-add-mock
KyleJune a35179a
Make assertion functions not return anything
KyleJune 7246c77
Fix description for assert function
KyleJune 7386899
Move mock files out of mock directory
KyleJune e8f0b94
Move _asserts.ts and _callbacks.ts into mock.ts
KyleJune 39c17fa
Add mock section to README.md with examples
KyleJune File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,324 @@ | ||
/** This module is browser compatible. */ | ||
|
||
import { | ||
assertEquals, | ||
AssertionError, | ||
assertIsError, | ||
assertRejects, | ||
} from "../asserts.ts"; | ||
import { Spy, SpyCall } from "./mock.ts"; | ||
|
||
/** An error related to spying on a function or instance method. */ | ||
export class MockError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = "MockError"; | ||
} | ||
} | ||
|
||
/** | ||
* Asserts that a spy is called as much as expected and no more. | ||
*/ | ||
export function assertSpyCalls< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
expectedCalls: number, | ||
) { | ||
try { | ||
assertEquals(spy.calls.length, expectedCalls); | ||
} catch (e) { | ||
assertIsError(e); | ||
let message = spy.calls.length < expectedCalls | ||
? "spy not called as much as expected:\n" | ||
: "spy called more than expected:\n"; | ||
message += e.message.split("\n").slice(1).join("\n"); | ||
throw new AssertionError(message); | ||
} | ||
} | ||
|
||
/** Call information recorded by a spy. */ | ||
export interface ExpectedSpyCall< | ||
// deno-lint-ignore no-explicit-any | ||
Self = any, | ||
// deno-lint-ignore no-explicit-any | ||
Args extends unknown[] = any[], | ||
// deno-lint-ignore no-explicit-any | ||
Return = any, | ||
> { | ||
/** Arguments passed to a function when called. */ | ||
args?: [...Args, ...unknown[]]; | ||
/** The instance that a method was called on. */ | ||
self?: Self; | ||
/** | ||
* The value that was returned by a function. | ||
* If you expect a promise to reject, expect error instead. | ||
*/ | ||
returned?: Return; | ||
error?: { | ||
/** The class for the error that was thrown by a function. */ | ||
// deno-lint-ignore no-explicit-any | ||
Class?: new (...args: any[]) => Error; | ||
/** Part of the message for the error that was thrown by a function. */ | ||
msgIncludes?: string; | ||
}; | ||
} | ||
|
||
/** | ||
* Asserts that a spy is called as expected. | ||
* Returns the call. | ||
*/ | ||
export function assertSpyCall< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
expected?: ExpectedSpyCall<Self, Args, Return>, | ||
) { | ||
if (spy.calls.length < (callIndex + 1)) { | ||
throw new AssertionError("spy not called as much as expected"); | ||
} | ||
const call: SpyCall = spy.calls[callIndex]; | ||
if (expected) { | ||
if (expected.args) { | ||
try { | ||
assertEquals(call.args, expected.args); | ||
} catch (e) { | ||
assertIsError(e); | ||
throw new AssertionError( | ||
"spy not called with expected args:\n" + | ||
e.message.split("\n").slice(1).join("\n"), | ||
); | ||
} | ||
} | ||
|
||
if ("self" in expected) { | ||
try { | ||
assertEquals(call.self, expected.self); | ||
} catch (e) { | ||
assertIsError(e); | ||
let message = expected.self | ||
? "spy not called as method on expected self:\n" | ||
: "spy not expected to be called as method on object:\n"; | ||
message += e.message.split("\n").slice(1).join("\n"); | ||
throw new AssertionError(message); | ||
} | ||
} | ||
|
||
if ("returned" in expected) { | ||
if ("error" in expected) { | ||
throw new TypeError( | ||
"do not expect error and return, only one should be expected", | ||
); | ||
} | ||
if (call.error) { | ||
throw new AssertionError( | ||
"spy call did not return expected value, an error was thrown.", | ||
); | ||
} | ||
try { | ||
assertEquals(call.returned, expected.returned); | ||
} catch (e) { | ||
assertIsError(e); | ||
throw new AssertionError( | ||
"spy call did not return expected value:\n" + | ||
e.message.split("\n").slice(1).join("\n"), | ||
); | ||
} | ||
} | ||
|
||
if ("error" in expected) { | ||
if ("returned" in call) { | ||
throw new AssertionError( | ||
"spy call did not throw an error, a value was returned.", | ||
); | ||
} | ||
assertIsError( | ||
call.error, | ||
expected.error?.Class, | ||
expected.error?.msgIncludes, | ||
); | ||
} | ||
} | ||
return call; | ||
} | ||
|
||
/** | ||
* Asserts that an async spy is called as expected. | ||
* Returns the call. | ||
*/ | ||
export async function assertSpyCallAsync< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
>( | ||
spy: Spy<Self, Args, Promise<Return>>, | ||
callIndex: number, | ||
expected?: ExpectedSpyCall<Self, Args, Promise<Return> | Return>, | ||
) { | ||
const expectedSync = expected && { ...expected }; | ||
if (expectedSync) { | ||
delete expectedSync.returned; | ||
delete expectedSync.error; | ||
} | ||
const call: SpyCall = assertSpyCall( | ||
spy, | ||
callIndex, | ||
expectedSync, | ||
); | ||
|
||
if (call.error) { | ||
throw new AssertionError( | ||
"spy call did not return a promise, an error was thrown.", | ||
); | ||
} | ||
if (call.returned !== Promise.resolve(call.returned)) { | ||
throw new AssertionError( | ||
"spy call did not return a promise, a value was returned.", | ||
); | ||
} | ||
|
||
if (expected) { | ||
if ("returned" in expected) { | ||
if ("error" in expected) { | ||
throw new TypeError( | ||
"do not expect error and return, only one should be expected", | ||
); | ||
} | ||
if (call.error) { | ||
throw new AssertionError( | ||
"spy call did not return expected value, an error was thrown.", | ||
); | ||
} | ||
let expectedResolved; | ||
try { | ||
expectedResolved = await expected.returned; | ||
} catch { | ||
throw new TypeError( | ||
"do not expect rejected promise, expect error instead", | ||
); | ||
} | ||
|
||
let resolved; | ||
try { | ||
resolved = await call.returned; | ||
} catch { | ||
throw new AssertionError("spy call returned promise was rejected"); | ||
} | ||
|
||
try { | ||
assertEquals(resolved, expectedResolved); | ||
} catch (e) { | ||
assertIsError(e); | ||
throw new AssertionError( | ||
"spy call did not resolve to expected value:\n" + | ||
e.message.split("\n").slice(1).join("\n"), | ||
); | ||
} | ||
} | ||
|
||
if ("error" in expected) { | ||
await assertRejects( | ||
() => Promise.resolve(call.returned), | ||
expected.error?.Class ?? Error, | ||
expected.error?.msgIncludes ?? "", | ||
); | ||
} | ||
} | ||
return call; | ||
} | ||
|
||
/** | ||
* Asserts that a spy is called with a specific arg as expected. | ||
* Returns the actual arg. | ||
*/ | ||
export function assertSpyCallArg< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
ExpectedArg, | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
argIndex: number, | ||
expected: ExpectedArg, | ||
): ExpectedArg { | ||
const call: SpyCall = assertSpyCall(spy, callIndex); | ||
const arg = call.args[argIndex]; | ||
assertEquals(arg, expected); | ||
return arg as ExpectedArg; | ||
} | ||
|
||
/** | ||
* Asserts that an spy is called with a specific range of args as expected. | ||
* If a start and end index is not provided, the expected will be compared against all args. | ||
* If a start is provided without an end index, the expected will be compared against all args from the start index to the end. | ||
* The end index is not included in the range of args that are compared. | ||
* Returns the actual args. | ||
*/ | ||
export function assertSpyCallArgs< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
ExpectedArgs extends unknown[], | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
expected: ExpectedArgs, | ||
): ExpectedArgs; | ||
export function assertSpyCallArgs< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
ExpectedArgs extends unknown[], | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
argsStart: number, | ||
expected: ExpectedArgs, | ||
): ExpectedArgs; | ||
export function assertSpyCallArgs< | ||
Self, | ||
Args extends unknown[], | ||
Return, | ||
ExpectedArgs extends unknown[], | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
argStart: number, | ||
argEnd: number, | ||
expected: ExpectedArgs, | ||
): ExpectedArgs; | ||
export function assertSpyCallArgs< | ||
ExpectedArgs extends unknown[], | ||
Args extends unknown[], | ||
Return, | ||
Self, | ||
>( | ||
spy: Spy<Self, Args, Return>, | ||
callIndex: number, | ||
argsStart?: number | ExpectedArgs, | ||
argsEnd?: number | ExpectedArgs, | ||
expected?: ExpectedArgs, | ||
): ExpectedArgs { | ||
const call: SpyCall = assertSpyCall(spy, callIndex); | ||
if (!expected) { | ||
expected = argsEnd as ExpectedArgs; | ||
argsEnd = undefined; | ||
} | ||
if (!expected) { | ||
expected = argsStart as ExpectedArgs; | ||
argsStart = undefined; | ||
} | ||
const args = typeof argsEnd === "number" | ||
? call.args.slice(argsStart as number, argsEnd) | ||
: typeof argsStart === "number" | ||
? call.args.slice(argsStart) | ||
: call.args; | ||
assertEquals(args, expected); | ||
return args as ExpectedArgs; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think returning value from assertion is confusing and less readable
(Note: We had similar discussion in the past about this design #1052 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also can get n-th call object from spy.calls[n]. I think there's no need of returning call object here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be good with keeping the third argument optional so that it can still be used for just checking if the spyFunc was called enough times but just having this assertion function changed to not returning a value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went ahead and made this change, the assert functions no longer return anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!