-
Notifications
You must be signed in to change notification settings - Fork 2
feat(function): Refactor FunctionAssertion and add ErrorAssertion #30
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
Merged
JoseLion
merged 2 commits into
master
from
function-assertion/refactor-and-error-assertion
Jun 29, 2022
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
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 hidden or 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,150 @@ | ||
| import { AssertionError } from "assert"; | ||
|
|
||
| import { Assertion } from "./Assertion"; | ||
|
|
||
| export class ErrorAssertion<T extends Error> extends Assertion<T> { | ||
|
|
||
| public constructor(actual: T) { | ||
| super(actual); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the error has exactly the passed error. | ||
| * | ||
| * @param message the message the error should contain | ||
| * @returns the assertion instance | ||
| */ | ||
| public toHaveMessage(message: string): this { | ||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| expected: message, | ||
| message: `Expected error to have the message: ${message}` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual, | ||
| message: `Expected error NOT to have the message: ${message}` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: this.actual.message === message, | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the error has a message that starts with the provided fragment | ||
| * | ||
| * @param fragment the fragment the message should start with | ||
| * @returns the assertion instance | ||
| */ | ||
| public toHaveMessageStartingWith(fragment: string): this { | ||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error to have a message starting with: ${fragment}` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error NOT to have a message starting with: ${fragment}` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: this.actual.message.startsWith(fragment), | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the error has a message that contains the provided fragment | ||
| * | ||
| * @param fragment the fragment the message should contain | ||
| * @returns the assertion instance | ||
| */ | ||
| public toHaveMessageContaining(fragment: string): this { | ||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error to have a message containing: ${fragment}` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error NOT to have a message containing: ${fragment}` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: this.actual.message.includes(fragment), | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the error has a message that ends with the provided fragment | ||
| * | ||
| * @param fragment the fragment the message should end with | ||
| * @returns the assertion instance | ||
| */ | ||
| public toHaveMessageEndingWith(fragment: string): this { | ||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error to have a message ending with: ${fragment}` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected error NOT to have a message ending with: ${fragment}` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: this.actual.message.endsWith(fragment), | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the error has a message taht matches the provided regular | ||
| * expression. | ||
| * | ||
| * @param regex the regular expression to match the error message | ||
| * @returns the assertion error | ||
| */ | ||
| public toHaveMessageMatching(regex: RegExp): this { | ||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected the error message to match the regex <${regex.source}>` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual, | ||
| message: `Expected the error message NOT to match the regex <${regex.source}>` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: regex.test(this.actual.message), | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the name of the error is the passed name. | ||
| * | ||
| * @param name the name of the error | ||
| * @returns the assertion instance | ||
| */ | ||
| public toHaveName(name: string): this { | ||
JoseLion marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const error = new AssertionError({ | ||
| actual: this.actual.message, | ||
| message: `Expected the error name to be <${name}>` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual, | ||
| message: `Expected the error name NOT to be <${name}>` | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: this.actual.name === name, | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
| } | ||
This file contains hidden or 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 |
|---|---|---|
| @@ -1,56 +1,159 @@ | ||
| import { AssertionError } from "assert"; | ||
|
|
||
| import { Assertion } from "./Assertion"; | ||
| import { ErrorAssertion } from "./ErrorAssertion"; | ||
| import { TypeFactory } from "./helpers/TypeFactories"; | ||
|
|
||
| export type AnyFunction = (...args: any[]) => any; | ||
|
|
||
| function functionExecution<T extends AnyFunction>(func: T): Error | undefined { | ||
| try { | ||
| func(); | ||
| return undefined; | ||
| } catch (error) { | ||
| return error instanceof Error | ||
| ? error | ||
| : Error(`The function threw something that is not an Error: ${error}`); | ||
| } | ||
| interface Class<T> extends Function { | ||
| prototype: T; | ||
| } | ||
|
|
||
| function assertion<E extends Error>(error: E | undefined , expectedError: E): boolean { | ||
| return !!error | ||
| && error?.name === expectedError.name | ||
| && error?.message === expectedError.message; | ||
| } | ||
| const NoThrow = Symbol("NoThrow"); | ||
|
|
||
| export class FunctionAssertion<T extends AnyFunction> extends Assertion<T> { | ||
|
|
||
| constructor(actual: T) { | ||
| super(actual); | ||
| } | ||
|
|
||
| private captureError(): unknown | typeof NoThrow { | ||
| try { | ||
| this.actual(); | ||
| return NoThrow; | ||
| } catch (error) { | ||
| return error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if the value throws an error. | ||
| * Check if the function throws anything when called. | ||
| * | ||
| * @returns the assertion instance | ||
| */ | ||
| public toThrowError<E extends Error>(expectedError?: E): this { | ||
| const expected = expectedError || new Error(); | ||
| const errorExecution = functionExecution(this.actual); | ||
| public toThrow(): this { | ||
| const captured = this.captureError(); | ||
| const error = new AssertionError({ | ||
| actual: this.actual, | ||
| expected, | ||
| message: `Expected to throw error <${expected.name}> with message <'${expected.message || ""}'>` | ||
| actual: captured, | ||
| message: "Expected the function to throw when called" | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: this.actual, | ||
| message: `Expected value to NOT throw error <${expected.name}> with message <'${expected.message || ""}'>` | ||
| actual: captured, | ||
| message: "Expected the function NOT to throw when called" | ||
| }); | ||
|
|
||
| return this.execute({ | ||
| assertWhen: expectedError | ||
| ? assertion(errorExecution, expected) | ||
| : errorExecution instanceof Error, | ||
| assertWhen: captured !== NoThrow, | ||
| error, | ||
| invertedError | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the function throws an {@link Error}. If the `ErrorType` is passed, | ||
| * it also checks if the error is an instance of the specific type. | ||
| * | ||
| * @example | ||
| * ``` | ||
| * expect(throwingFunction) | ||
| * .toThrowError() | ||
| * .toHaveMessage("Oops! Something went wrong...") | ||
| * | ||
| * expect(myCustomFunction) | ||
| * .toThrowError(MyCustomError) | ||
| * .toHaveMessage("Something failed!"); | ||
| * ``` | ||
| * | ||
| * @param ErrorType optional error type constructor to check the thrown error | ||
| * against. If is not provided, it defaults to {@link Error} | ||
| * @returns a new {@link ErrorAssertion} to assert over the error | ||
| */ | ||
| public toThrowError(): ErrorAssertion<Error>; | ||
| public toThrowError<E extends Error>(ExpectedType: Class<E>): ErrorAssertion<E>; | ||
| public toThrowError<E extends Error>(ExpectedType?: Class<E>): ErrorAssertion<E> { | ||
| const captured = this.captureError(); | ||
|
|
||
| if (captured === NoThrow) { | ||
| throw new AssertionError({ | ||
| actual: captured, | ||
| message: "Expected the function to throw when called" | ||
| }); | ||
| } | ||
|
|
||
| const ErrorType = ExpectedType ?? Error; | ||
| const error = new AssertionError({ | ||
| actual: captured, | ||
| message: `Expected the function to throw an error instance of <${ErrorType.name}>` | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: captured, | ||
| message: `Expected the function NOT to throw an error instance of <${ErrorType.name}>` | ||
| }); | ||
|
|
||
| this.execute({ | ||
| assertWhen: captured instanceof ErrorType, | ||
| error, | ||
| invertedError | ||
| }); | ||
|
|
||
| return new ErrorAssertion(captured as E); | ||
JoseLion marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Check if the function throws a non-error value when called. Additionally, | ||
| * you can pass a {@link TypeFactory} in the second argument so the returned | ||
| * assertion is for the specific value type. Otherwise, a basic | ||
| * {@link Assertion Assertion<unknown>} instance is returned. | ||
| * | ||
| * @example | ||
| * ``` | ||
| * expect(raiseValue) | ||
| * .toThrowValue() | ||
| * .toBeEqual(someValue); | ||
| * | ||
| * expect(raiseExitCode) | ||
| * .toThrowValue(TypeFactories.Number) | ||
| * .toBeNegative(); | ||
| * ``` | ||
| * | ||
| * @param expected the value the function is expected to throw | ||
| * @param typeFactory optional type factory to perform more specific | ||
| * assertions over the thrown value | ||
| * @returns the factory assertion or a basic assertion instance | ||
| */ | ||
| public toThrowValue<S, A extends Assertion<S>>(typeFactory?: TypeFactory<S, A>): A { | ||
JoseLion marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const captured = this.captureError(); | ||
|
|
||
| if (captured === NoThrow) { | ||
| throw new AssertionError({ | ||
| actual: captured, | ||
| message: "Expected the function to throw a value" | ||
| }); | ||
| } | ||
|
|
||
| const error = new AssertionError({ | ||
| actual: captured, | ||
| message: typeFactory | ||
| ? `Expected the function to throw a value of type "${typeFactory.typeName}"` | ||
| : "Expected the function to throw a value" | ||
| }); | ||
| const invertedError = new AssertionError({ | ||
| actual: captured, | ||
| message: typeFactory | ||
| ? `Expected the function NOT to throw a value of type "${typeFactory.typeName}"` | ||
| : "Expected the function NOT to throw a value" | ||
| }); | ||
| const isTypeMatch = typeFactory?.predicate(captured) ?? true; | ||
|
|
||
| this.execute({ | ||
| assertWhen: captured !== NoThrow && isTypeMatch, | ||
| error, | ||
| invertedError | ||
| }); | ||
|
|
||
| return typeFactory?.predicate(captured) | ||
| ? new typeFactory.Factory(captured) | ||
| : new Assertion(captured) as A; | ||
| } | ||
| } | ||
This file contains hidden or 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
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.
Uh oh!
There was an error while loading. Please reload this page.