UNPKG

@assertive-ts/sinon

Version:

Assertive.ts plugin for Sinon assertions

351 lines (323 loc) 10.2 kB
import { Assertion, AssertionError } from "@assertive-ts/core"; import isDeepEqual from "fast-deep-equal"; import { SinonSpy } from "sinon"; import { SinonSpyCallAssertion } from "./SinonSpyCallAssertion"; import { callTimes, numeral, prettify } from "./helpers/messages"; /** * Encapsulates assertion methods applicable to {@link SinonSpy} instances. * This includes `Sinon.spy(..)`, `Sinon.stub(..)`, `Sinon.mock()` and * `Sinon.fake(..)` as all of them extend from a SinonSpy. * * @param A the arguments type of the spied function * @param R the type return type of the spied function */ export class SinonSpyAssertion<A extends unknown[], R> extends Assertion<SinonSpy<A, R>> { public constructor(actual: SinonSpy<A, R>) { super(actual); } /** * Check if the spy was called exactly a number of times. If the argument is * omited it defaults to one. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).tobeCalled(); // exactly once * expect(spy).toBeCalled(3); // exacty 3 times * ``` * * @param times the number of times the spy is called. Defaults to `1`. * @returns the assertion instance */ public toBeCalled(times: number = 1): this { if (times < 0) { throw new Error("Spy cannot be called less than zero times!"); } const { name, callCount } = this.actual; const error = new AssertionError({ actual: callCount, expected: times, message: `Expected <${name}> to be called ${numeral(times)}, but it was ${callTimes(callCount)}`, }); const invertedError = new AssertionError({ actual: this.actual, message: `Expected <${name}> NOT to be called ${numeral(times)}, but it was ${callTimes(callCount)}`, }); return this.execute({ assertWhen: callCount === times, error, invertedError, }); } /** * Check if the spy was called exactly once and return a * {@link SinonSpyAssertion} instance of the first call. This allows to make * more specific verifications over the spy. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy) * .tobeCalledOnce() * .toReturn(..); * ``` * * @returns a {@link SinonSpyCallAssertion} instance of the first call */ public toBeCalledOnce(): SinonSpyCallAssertion<A, R> { const { actual } = this.toBeCalled(1); return new SinonSpyCallAssertion(actual.firstCall); } /** * Check if the spy was called exactly twice. This is a more explicit name of * calling `.toBeCalled(2)`. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).tobeCalledTwice(); * ``` * * @returns the assertion instance */ public toBeCalledTwice(): this { return this.toBeCalled(2); } /** * Check if the spy was called exactly thrice. This is a more explicit name of * calling `.toBeCalled(3)`. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toBeCalledThrice(); * ``` * * @returns the assertion instance */ public toBeCalledThrice(): this { return this.toBeCalled(3); } /** * Check if the spy was called at least a specific number of times. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toBeCalledAtLeast(3); * ``` * * @param times the number of times the spy is at least called * @returns the assertion instance */ public toBeCalledAtLeast(times: number): this { if (times < 0) { throw new Error("Spy cannot be called less than zero times!"); } const { name, callCount } = this.actual; const error = new AssertionError({ actual: callCount, message: `Expeceted <${name}> to be called at least ${numeral(times)}, but it was ${callTimes(callCount)}`, }); const invertedError = new AssertionError({ actual: callCount, message: `Expeceted <${name}> NOT to be called at least ${numeral(times)}, but it was ${callTimes(callCount)}`, }); return this.execute({ assertWhen: callCount >= times, error, invertedError, }); } /** * Check if the spy was called at most a specific number of times. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toBeCalledAtMost(2); * ``` * * @param times the number of times the spy is at most called * @returns the assertion instance */ public toBeCalledAtMost(times: number): this { if (times < 0) { throw new Error("Spy cannot be called less than zero times!"); } const { name, callCount } = this.actual; const error = new AssertionError({ actual: callCount, message: `Expeceted <${name}> to be called at most ${numeral(times)}, but it was ${callTimes(callCount)}`, }); const invertedError = new AssertionError({ actual: callCount, message: `Expeceted <${name}> NOT to be called at most ${numeral(times)}, but it was ${callTimes(callCount)}`, }); return this.execute({ assertWhen: callCount <= times, error, invertedError, }); } /** * Check if the spy was never called. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toNeverBeCalled(); * ``` * * @returns the assertion instance */ public toNeverBeCalled(): this { const { name, callCount } = this.actual; const error = new AssertionError({ actual: callCount, expected: 0, message: `Expected <${name}> to be never called, but it was ${callTimes(callCount)}`, }); const invertedError = new AssertionError({ actual: callCount, message: `Expected <${name}> NOT to be never called, but it was ${callTimes(callCount)}`, }); return this.execute({ assertWhen: callCount === 0, error, invertedError, }); } /** * Check if any of the calls to this spy have the expected arguments. Each * argument is compared with a strict-deep-equals strategy and must be in * the exact same order as called. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toHaveArgs("foo", 3, true); * ``` * * @param expected the expected arguments passed to any call * @returns the assertion instance */ public toHaveArgs(...expected: A): this { const { name, args } = this.actual; const prettyArgs = expected.map(prettify).join(", "); const error = new AssertionError({ expected, message: `Expected <${name}> to be called with <${prettyArgs}>`, }); const invertedError = new AssertionError({ actual: args, message: `Expected <${name}> NOT to be called with <${prettyArgs}>`, }); return this.execute({ assertWhen: args.some(callArgs => isDeepEqual(callArgs, expected)), error, invertedError, }); } /** * Check if any of the calls to this spy returns the expected value. The * value is compared with a strict-deep-equals strategy. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toReturn({ res: "ok" }); * ``` * * @param expected the expected value returned by any call * @returns the assertion instance */ public toReturn(expected: R): this { const { name, returnValues } = this.actual; const error = new AssertionError({ expected, message: `Expected <${name}> to return <${prettify(expected)}> when called`, }); const invertedError = new AssertionError({ actual: returnValues, message: `Expected <${name}> NOT to return <${prettify(expected)}> when called`, }); return this.execute({ assertWhen: returnValues.some(value => isDeepEqual(value, expected)), error, invertedError, }); } /** * Check if any of the calls to this spy throws an exception. The thrown * values are compared with a strict-deep-equals strategy. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy).toThrow("I'm not an error"); * expect(spy).toThrow(new Error("I'm an error")); * ``` * * @param exception the exception thrown by any call * @returns the assertion instance */ public toThrow(exception?: unknown): this { const { name, exceptions } = this.actual; const errorCount = exceptions?.length ?? 0; const expected = exception !== undefined ? `<${prettify(exception)}>` : "when called"; const error = new AssertionError({ expected: exception, message: `Expected <${name}> to throw ${expected}`, }); const invertedError = new AssertionError({ actual: exceptions, message: `Expected <${name}> NOT to throw ${expected}`, }); return this.execute({ assertWhen: exception !== undefined ? exceptions?.some(ex => isDeepEqual(ex, exception)) : errorCount > 0, error, invertedError, }); } /** * Retrieves a specific call of the spy, checking first if there's at least * that number of calls. Then returns a {@link SinonSpyCallAssertion} * instance of that call. This allows more specific verifications over the * spy when this is called more than once. * * @example * ``` * const spy = Sinon.spy(..); // .stub(..) / .mock(..) / .fake(..) * * expect(spy) * .call(7) * .toHaveArgs(..); // check over the 7th call to this spy * ``` * * @param count The spy call to retrieve. Where `1` means the first call, `2` * the second call, and so on. * @returns a {@link SinonSpyCallAssertion} instance of the call */ public call(count: number): SinonSpyCallAssertion<A, R> { if (count === 0) { throw new Error("It's not possible to access no call at all!"); } this.toBeCalledAtLeast(count); return new SinonSpyCallAssertion(this.actual.getCall(count - 1)); } }