From 1f9e70e404d18911f4bb61d2105fb31414cfd721 Mon Sep 17 00:00:00 2001 From: Maciej Radzikowski Date: Mon, 27 Jun 2022 22:35:59 +0200 Subject: [PATCH 1/3] feat: allow usage of asymmetric matchers in built-in Jest matchers --- package.json | 4 + src/jestMatchers.ts | 197 +++++++++++++------------- test/jestMatchers.test.ts | 282 ++++++++++++++++++++------------------ yarn.lock | 2 +- 4 files changed, 253 insertions(+), 232 deletions(-) diff --git a/package.json b/package.json index 1f1d221..945808b 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "husky": "7.0.4", "jest": "28.1.1", "lint-staged": "11.1.2", + "pretty-format": "28.1.1", "rimraf": "3.0.2", "size-limit": "5.0.3", "standard-version": "9.3.1", @@ -84,6 +85,9 @@ "modulePathIgnorePatterns": [ "test-e2e", "verdaccio-storage" + ], + "snapshotSerializers": [ + "./node_modules/pretty-format/build/plugins/ConvertAnsi.js" ] }, "lint-staged": { diff --git a/src/jestMatchers.ts b/src/jestMatchers.ts index b0a108b..590d276 100644 --- a/src/jestMatchers.ts +++ b/src/jestMatchers.ts @@ -1,10 +1,10 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-interface */ import assert from 'assert'; import type {MetadataBearer} from '@aws-sdk/types'; import type {AwsCommand, AwsStub} from './awsClientStub'; import type {SinonSpyCall} from 'sinon'; -export interface AwsSdkJestMockBaseMatchers extends Record { +interface AwsSdkJestMockBaseMatchers extends Record { /** * Asserts {@link AwsStub Aws Client Mock} received a {@link command} exact number of {@link times} * @@ -28,8 +28,7 @@ export interface AwsSdkJestMockBaseMatchers extends Record { ): R; /** - * Asserts {@link AwsStub Aws Client Mock} received a {@link command} at leas one time with input - * matching {@link input} + * Asserts {@link AwsStub Aws Client Mock} received a {@link command} at least one time with matching {@link input} * * @param command aws-sdk command constructor * @param input @@ -56,7 +55,7 @@ export interface AwsSdkJestMockBaseMatchers extends Record { ): R; } -export interface AwsSdkJestMockAliasMatchers { +interface AwsSdkJestMockAliasMatchers { /** * Asserts {@link AwsStub Aws Client Mock} received a {@link command} exact number of {@link times} * @@ -82,8 +81,7 @@ export interface AwsSdkJestMockAliasMatchers { ): R; /** - * Asserts {@link AwsStub Aws Client Mock} received a {@link command} at leas one time with input - * matching {@link input} + * Asserts {@link AwsStub Aws Client Mock} received a {@link command} at least one time with matching {@link input} * * @alias {@link AwsSdkJestMockBaseMatchers.toHaveReceivedCommandWith} * @param command aws-sdk command constructor @@ -141,7 +139,6 @@ export interface AwsSdkJestMockMatchers extends AwsSdkJestMockBaseMatchers declare global { namespace jest { - // eslint-disable-next-line @typescript-eslint/no-empty-interface interface Matchers extends AwsSdkJestMockMatchers { } } @@ -153,7 +150,6 @@ type AnySpyCall = SinonSpyCall<[AnyCommand]>; type MessageFunctionParams = { cmd: string; client: string; - calls: AnySpyCall[]; commandCalls: AnySpyCall[]; data: CheckData; notPrefix: string; @@ -161,31 +157,21 @@ type MessageFunctionParams = { /** * Prettyprints command calls for message - * - * @param ctx - * @param calls - * @returns */ -function printCalls(ctx: jest.MatcherContext, calls: AnySpyCall[]): string[] { - return calls.length > 0 ? [ - 'Calls:', - '', - ...calls.map( - (c, i) => - ` ${i + 1}. ${c.args[0].constructor.name}: ${ctx.utils.printReceived( - c.args[0].input, - )}`, - )] : []; -} +const printCalls = (ctx: jest.MatcherContext, calls: AnySpyCall[]): string[] => + calls.length > 0 + ? [ + '', + 'Calls:', + ...calls.map( + (c, i) => + ` ${i + 1}. ${c.args[0].constructor.name}: ${ctx.utils.printReceived( + c.args[0].input, + )}`, + )] + : []; -export function processMatch({ - ctx, - mockClient, - command, - check, - input, - message, -}: { +const processMatch = ({ctx, mockClient, command, check, message}: { ctx: jest.MatcherContext; mockClient: ClientMock; command: new () => AnyCommand; @@ -193,84 +179,77 @@ export function processMatch({ pass: boolean; data: CheckData; }; - input: Record | undefined; message: (params: MessageFunctionParams) => string[]; -}): jest.CustomMatcherResult { +}): jest.CustomMatcherResult => { assert( command && typeof command === 'function' && typeof command.name === 'string' && command.name.length > 0, - 'Command must be valid AWS Sdk Command', + 'Command must be valid AWS SDK Command', ); const calls = mockClient.calls(); - const commandCalls = mockClient.commandCalls(command, input); + const commandCalls = mockClient.commandCalls(command); + const {pass, data} = check({calls, commandCalls}); const msg = (): string => { const cmd = ctx.utils.printExpected(command.name); const client = mockClient.clientName(); - const msgParams: MessageFunctionParams = { - calls, - client, - cmd, - data, - commandCalls, - notPrefix: ctx.isNot ? 'not ' : '', - }; - - return message(msgParams).join('\n'); + return [ + ...message({ + client, + cmd, + data, + commandCalls, + notPrefix: ctx.isNot ? 'not ' : '', + }), + ...printCalls(ctx, calls), + ].join('\n'); }; return {pass, message: msg}; -} +}; -/* Using them for testing */ -export const baseMatchers: { [P in keyof AwsSdkJestMockBaseMatchers]: jest.CustomMatcher } = { +const baseMatchers: { [P in keyof AwsSdkJestMockBaseMatchers]: jest.CustomMatcher } = { /** - * implementation of {@link AwsSdkJestMockMatchers.toHaveReceivedCommandTimes} matcher + * implementation of {@link AwsSdkJestMockMatchers.toHaveReceivedCommand} matcher */ - toHaveReceivedCommandTimes( + toHaveReceivedCommand( this: jest.MatcherContext, mockClient: ClientMock, command: new () => AnyCommand, - expectedCalls: number, ) { return processMatch({ ctx: this, mockClient, command, - input: undefined, - check: ({commandCalls}) => ({pass: commandCalls.length === expectedCalls, data: {}}), - message: ({client, cmd, commandCalls, notPrefix}) => [ - `Expected ${client} to ${notPrefix}receive ${cmd} ${this.utils.printExpected( - expectedCalls, - )} times`, + check: ({commandCalls}) => ({pass: commandCalls.length > 0, data: undefined}), + message: ({client, cmd, notPrefix, commandCalls}) => [ + `Expected ${client} to ${notPrefix}receive ${cmd}`, `${client} received ${cmd} ${this.utils.printReceived(commandCalls.length)} times`, - ...printCalls(this, commandCalls), ], }); }, /** - * implementation of {@link AwsSdkJestMockMatchers.toHaveReceivedCommand} matcher + * implementation of {@link AwsSdkJestMockMatchers.toHaveReceivedCommandTimes} matcher */ - toHaveReceivedCommand( + toHaveReceivedCommandTimes( this: jest.MatcherContext, mockClient: ClientMock, command: new () => AnyCommand, + expectedCalls: number, ) { return processMatch({ ctx: this, mockClient, command, - input: undefined, - check: ({commandCalls}) => ({pass: commandCalls.length > 0, data: {}}), - message: ({client, cmd, notPrefix, commandCalls}) => [ - `Expected ${client} to ${notPrefix}receive ${cmd}`, + check: ({commandCalls}) => ({pass: commandCalls.length === expectedCalls, data: undefined}), + message: ({client, cmd, commandCalls, notPrefix}) => [ + `Expected ${client} to ${notPrefix}receive ${cmd} ${this.utils.printExpected(expectedCalls)} times`, `${client} received ${cmd} ${this.utils.printReceived(commandCalls.length)} times`, - ...printCalls(this, commandCalls), ], }); }, @@ -283,18 +262,30 @@ export const baseMatchers: { [P in keyof AwsSdkJestMockBaseMatchers]: j command: new () => AnyCommand, input: Record, ) { - return processMatch({ + return processMatch<{ matchCount: number }>({ ctx: this, mockClient, command, - input, - check: ({commandCalls}) => ({pass: commandCalls.length > 0, data: {}}), - message: ({client, cmd, calls, notPrefix, commandCalls}) => [ - `Expected ${client} to ${notPrefix}receive ${cmd} with ${this.utils.printExpected( - input, - )}`, - `${client} received ${cmd} ${this.utils.printReceived(commandCalls.length)} times`, - ...printCalls(this, calls), + check: ({commandCalls}) => { + const matchCount = commandCalls + .map(call => call.args[0].input) // eslint-disable-line @typescript-eslint/no-unsafe-return + .map(received => { + try { + expect(received).toEqual( + expect.objectContaining(input), + ); + return true; + } catch (e) { + return false; + } + }) + .reduce((acc, val) => acc + Number(val), 0); + + return {pass: matchCount > 0, data: {matchCount}}; + }, + message: ({client, cmd, notPrefix, data}) => [ + `Expected ${client} to ${notPrefix}receive ${cmd} with ${this.utils.printExpected(input)}`, + `${client} received matching ${cmd} ${this.utils.printReceived(data.matchCount)} times`, ], }); }, @@ -310,43 +301,57 @@ export const baseMatchers: { [P in keyof AwsSdkJestMockBaseMatchers]: j ) { assert( call && typeof call === 'number' && call > 0, - 'Call number must be a number and greater as 0', + 'Call number must be a number greater than 0', ); - return processMatch<{ received: AnyCommand; cmd: string }>({ + return processMatch<{ received: AnyCommand | undefined }>({ ctx: this, mockClient, command, check: ({calls}) => { + if (calls.length < call) { + return {pass: false, data: {received: undefined}}; + } + const received = calls[call - 1].args[0]; + + let pass = false; + if (received instanceof command) { + try { + expect(received.input).toEqual( + expect.objectContaining(input), + ); + pass = true; + } catch (e) { // eslint-disable-line no-empty + } + } + return { - pass: - received instanceof command && this.equals(received.input, input), - data: { - received, - cmd: this.utils.printReceived(received.constructor.name), - }, + pass, + data: {received}, }; }, - input, - message: ({cmd, client, calls, data, notPrefix}) => [ - `Expected ${client} to ${notPrefix}receive ${call}. ${cmd}`, - `${client} received ${call}. ${data.cmd} with input`, - this.utils.printDiffOrStringify( - input, - data.received.input, - 'Expected', - 'Received', - false, - ), - ...printCalls(this, calls), + message: ({cmd, client, data, notPrefix}) => [ + `Expected ${client} to ${notPrefix}receive ${call}. ${cmd} with ${this.utils.printExpected(input)}`, + ...(data.received + ? [ + `${client} received ${this.utils.printReceived(data.received.constructor.name)} with input:`, + this.utils.printDiffOrStringify( + input, + data.received.input, + 'Expected', + 'Received', + false, + ), + ] + : []), ], }); }, }; /* typing ensures keys matching */ -export const aliasMatchers: { [P in keyof AwsSdkJestMockAliasMatchers]: jest.CustomMatcher } = { +const aliasMatchers: { [P in keyof AwsSdkJestMockAliasMatchers]: jest.CustomMatcher } = { toReceiveCommandTimes: baseMatchers.toHaveReceivedCommandTimes, toReceiveCommand: baseMatchers.toHaveReceivedCommand, toReceiveCommandWith: baseMatchers.toHaveReceivedCommandWith, diff --git a/test/jestMatchers.test.ts b/test/jestMatchers.test.ts index 323b5f0..6da3e94 100644 --- a/test/jestMatchers.test.ts +++ b/test/jestMatchers.test.ts @@ -1,218 +1,230 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ import {AwsClientStub, mockClient} from '../src'; import {PublishCommand, SNSClient} from '@aws-sdk/client-sns'; -import {publishCmd1, publishCmd2, uuid1} from './fixtures'; -import {aliasMatchers, baseMatchers} from '../src/jestMatchers'; -import {inspect} from 'util'; +import {publishCmd1, publishCmd2} from './fixtures'; let snsMock: AwsClientStub; -const contextMock = { - isNot: false, - equals: jest.fn(), - utils: { - printExpected: jest.fn(), - printReceived: jest.fn(), - printDiffOrStringify: jest.fn(), - }, -}; - beforeEach(() => { snsMock = mockClient(SNSClient); - - contextMock.isNot = false; - contextMock.equals.mockReturnValue(true); - contextMock.utils.printExpected.mockImplementation((v) => inspect(v, {compact: true})); - contextMock.utils.printReceived.mockImplementation((v) => inspect(v, {compact: true})); - contextMock.utils.printDiffOrStringify.mockImplementation((a, b) => [ - inspect(a, {compact: true}), - inspect(b, {compact: true}), - ].join('\n')); }); afterEach(() => { snsMock.restore(); }); -describe('matcher aliases', () => { - it('adds matcher aliases', () => { - expect(aliasMatchers.toReceiveCommand).toBe(baseMatchers.toHaveReceivedCommand); - expect(aliasMatchers.toReceiveCommandTimes).toBe(baseMatchers.toHaveReceivedCommandTimes); - expect(aliasMatchers.toReceiveCommandWith).toBe(baseMatchers.toHaveReceivedCommandWith); - expect(aliasMatchers.toReceiveNthCommandWith).toBe(baseMatchers.toHaveReceivedNthCommandWith); +describe('toHaveReceivedCommand', () => { + it('passes on receiving Command', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + + expect(() => expect(snsMock).toHaveReceivedCommand(PublishCommand)).not.toThrow(); }); -}); -describe('toHaveReceivedCommandTimes', () => { - it('matches calls count', async () => { - snsMock.resolves({ - MessageId: uuid1, - }); + it('fails on not receiving Command', () => { + expect(() => expect(snsMock).toHaveReceivedCommand(PublishCommand)).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive \\"PublishCommand\\" +SNSClient received \\"PublishCommand\\" 0 times" +`); + }); + it('fails on receiving Command with not', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedCommandTimes.call(contextMock as any, snsMock, PublishCommand, 2) as jest.CustomMatcherResult; - expect(match.pass).toBeFalsy(); + expect(() => expect(snsMock).not.toHaveReceivedCommand(PublishCommand)).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to not receive \\"PublishCommand\\" +SNSClient received \\"PublishCommand\\" 1 times - expect(match.message()).toEqual(`Expected SNSClient to receive 'PublishCommand' 2 times -SNSClient received 'PublishCommand' 1 times Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); + }); +}); - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); +describe('toHaveReceivedCommandTimes', () => { + it('passes on receiving Command twice', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + await sns.send(publishCmd2); + expect(() => expect(snsMock).toHaveReceivedCommandTimes(PublishCommand, 2)).not.toThrow(); }); - it('matches not calls count', async () => { - contextMock.isNot = true; + it('fails on not receiving Command twice', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); - snsMock.resolves({ - MessageId: uuid1, - }); + expect(() => expect(snsMock).toHaveReceivedCommandTimes(PublishCommand, 2)).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive \\"PublishCommand\\" 2 times +SNSClient received \\"PublishCommand\\" 1 times + +Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); + }); + it('fails on receiving Command twice with not', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedCommandTimes.call(contextMock as any, snsMock, PublishCommand, 2) as jest.CustomMatcherResult; - expect(match.pass).toBeTruthy(); + expect(() => expect(snsMock).not.toHaveReceivedCommandTimes(PublishCommand, 2)).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to not receive \\"PublishCommand\\" 2 times +SNSClient received \\"PublishCommand\\" 2 times - expect(match.message()).toEqual(`Expected SNSClient to not receive 'PublishCommand' 2 times -SNSClient received 'PublishCommand' 2 times Calls: - - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } - 2. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"} + 2. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); }); -describe('toHaveReceivedCommand', () => { - it('matches received', () => { - snsMock.resolves({ - MessageId: uuid1, - }); - - const match = baseMatchers.toHaveReceivedCommand.call(contextMock as any, snsMock, PublishCommand, 2) as jest.CustomMatcherResult; - expect(match.pass).toBeFalsy(); +describe('toHaveReceivedCommandWith', () => { + it('passes on receiving Command with partial match', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + await sns.send(publishCmd2); - expect(match.message()).toEqual(`Expected SNSClient to receive 'PublishCommand' -SNSClient received 'PublishCommand' 0 times`); + expect(() => expect(snsMock).toHaveReceivedCommandWith(PublishCommand, {Message: publishCmd2.input.Message})).not.toThrow(); }); - it('matches not received', async () => { - contextMock.isNot = true; - - snsMock.resolves({ - MessageId: uuid1, - }); - + it('fails on not receiving Command', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedCommand.call(contextMock as any, snsMock, PublishCommand, 2) as jest.CustomMatcherResult; - expect(match.pass).toBeTruthy(); + expect(() => expect(snsMock).toHaveReceivedCommandWith(PublishCommand, {Message: publishCmd2.input.Message})).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive \\"PublishCommand\\" with {\\"Message\\": \\"second mock message\\"} +SNSClient received matching \\"PublishCommand\\" 0 times - expect(match.message()).toEqual(`Expected SNSClient to not receive 'PublishCommand' -SNSClient received 'PublishCommand' 1 times Calls: - - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); -}); - -describe('toHaveReceivedCommandWith', () => { - it('matches received', async () => { - snsMock.resolves({ - MessageId: uuid1, - }); + it('fails on receiving Command with partial match with not', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedCommandWith.call(contextMock as any, - snsMock, PublishCommand, - publishCmd2.input, - ) as jest.CustomMatcherResult; - - expect(match.pass).toBeFalsy(); + expect(() => expect(snsMock).not.toHaveReceivedCommandWith(PublishCommand, {Message: publishCmd1.input.Message})).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to not receive \\"PublishCommand\\" with {\\"Message\\": \\"mock message\\"} +SNSClient received matching \\"PublishCommand\\" 1 times - expect(match.message()).toEqual(`Expected SNSClient to receive 'PublishCommand' with { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', - Message: 'second mock message' } -SNSClient received 'PublishCommand' 0 times Calls: - - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); - it('matches not received', async () => { - contextMock.isNot = true; + it('passes on match with asymmetric matcher', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); - snsMock.resolves({ - MessageId: uuid1, - }); + expect(() => expect(snsMock).toHaveReceivedCommandWith(PublishCommand, { + Message: expect.stringMatching(/message/), // eslint-disable-line @typescript-eslint/no-unsafe-assignment + })).not.toThrow(); + }); + it('fails on unmatch with asymmetric matcher', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedCommandWith.call(contextMock as any, snsMock, PublishCommand, publishCmd1.input) as jest.CustomMatcherResult; - expect(match.pass).toBeTruthy(); + expect(() => expect(snsMock).toHaveReceivedCommandWith(PublishCommand, { + Message: expect.stringMatching(/qq/), // eslint-disable-line @typescript-eslint/no-unsafe-assignment + })).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive \\"PublishCommand\\" with {\\"Message\\": StringMatching /qq/} +SNSClient received matching \\"PublishCommand\\" 0 times - expect(match.message()).toEqual(`Expected SNSClient to not receive 'PublishCommand' with { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } -SNSClient received 'PublishCommand' 1 times Calls: - - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); }); -describe('toHaveNthReceivedCommandWith', () => { - it('matches received', async () => { - contextMock.equals.mockReturnValue(false); +describe('toHaveReceivedNthCommandWith', () => { + it('passes on receiving second Command with partial match', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + await sns.send(publishCmd2); - snsMock.resolves({ - MessageId: uuid1, - }); + expect(() => expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, {Message: publishCmd2.input.Message})).not.toThrow(); + }); + it('fails on not receiving second Command', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); - await sns.send(publishCmd2); + await sns.send(publishCmd1); - const match = baseMatchers.toHaveReceivedNthCommandWith.call(contextMock as any, - snsMock, 1, PublishCommand, - publishCmd2.input, - ) as jest.CustomMatcherResult; + expect(() => expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, {Message: publishCmd2.input.Message})).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive 2. \\"PublishCommand\\" with {\\"Message\\": \\"second mock message\\"} +SNSClient received \\"PublishCommand\\" with input: +- Expected - 1 ++ Received + 2 - expect(match.pass).toBeFalsy(); + Object { +- \\"Message\\": \\"second mock message\\", ++ \\"Message\\": \\"mock message\\", ++ \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\", + } - expect(match.message()).toEqual(`Expected SNSClient to receive 1. 'PublishCommand' -SNSClient received 1. 'PublishCommand' with input -{ TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', - Message: 'second mock message' } -{ TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"} + 2. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); + }); + + it('fails on receiving second Command with not', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + await sns.send(publishCmd2); + + expect(() => expect(snsMock).not.toHaveReceivedNthCommandWith(2, PublishCommand, {Message: publishCmd2.input.Message})).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to not receive 2. \\"PublishCommand\\" with {\\"Message\\": \\"second mock message\\"} +SNSClient received \\"PublishCommand\\" with input: +- Expected - 0 ++ Received + 1 - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } - 2. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', - Message: 'second mock message' }`); + Object { + \\"Message\\": \\"second mock message\\", ++ \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\", + } + +Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"} + 2. PublishCommand: {\\"Message\\": \\"second mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); - it('matches not received', async () => { - contextMock.isNot = true; + it('passes on match with asymmetric matcher', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + await sns.send(publishCmd2); - snsMock.resolves({MessageId: uuid1}); + expect(() => expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, { + Message: expect.stringMatching(/second/), // eslint-disable-line @typescript-eslint/no-unsafe-assignment + })).not.toThrow(); + }); + it('fails on unmatch with asymmetric matcher', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1); + await sns.send(publishCmd2); - const match = baseMatchers.toHaveReceivedNthCommandWith.call(contextMock as any, snsMock, 1, PublishCommand, publishCmd1.input) as jest.CustomMatcherResult; - expect(match.pass).toBeTruthy(); + expect(() => expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, { + Message: expect.stringMatching(/qq/), // eslint-disable-line @typescript-eslint/no-unsafe-assignment + })).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive 2. \\"PublishCommand\\" with {\\"Message\\": StringMatching /qq/} +SNSClient received \\"PublishCommand\\" with input: +- Expected - 1 ++ Received + 2 - expect(match.message()).toEqual(`Expected SNSClient to not receive 1. 'PublishCommand' -SNSClient received 1. 'PublishCommand' with input -{ TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } -{ TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' } -Calls: + Object { +- \\"Message\\": StringMatching /qq/, ++ \\"Message\\": \\"second mock message\\", ++ \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\", + } - 1. PublishCommand: { TopicArn: 'arn:aws:sns:us-east-1:111111111111:MyTopic', Message: 'mock message' }`); +Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"} + 2. PublishCommand: {\\"Message\\": \\"second mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); }); }); diff --git a/yarn.lock b/yarn.lock index bf15e33..c349345 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8218,7 +8218,7 @@ prettier-bytes@^1.0.4: resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6" integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY= -pretty-format@^28.0.0, pretty-format@^28.1.1: +pretty-format@28.1.1, pretty-format@^28.0.0, pretty-format@^28.1.1: version "28.1.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.1.tgz#f731530394e0f7fcd95aba6b43c50e02d86b95cb" integrity sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw== From 6f8988695bb69385bfcbef8e47653be6296bba07 Mon Sep 17 00:00:00 2001 From: Maciej Radzikowski Date: Mon, 15 Aug 2022 12:47:57 +0200 Subject: [PATCH 2/3] test: fix Jest output colors even not on TTY --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 932fe72..eeb35cc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "prepare": "husky install", "ci": "yarn install --frozen-lockfile", "pretest": "rimraf coverage/", - "test": "jest --coverage", + "test": "jest --coverage --colors", "test-types": "tsd", "test-e2e": "ts-node test-e2e/simple/run.ts", "lint": "eslint .", From eac04be127897b14dda12fb3fd009e72edc27476 Mon Sep 17 00:00:00 2001 From: Maciej Radzikowski Date: Mon, 15 Aug 2022 13:05:10 +0200 Subject: [PATCH 3/3] test: jest matcher toHaveReceivedNthCommandWith with less commands --- test/jestMatchers.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/jestMatchers.test.ts b/test/jestMatchers.test.ts index 6da3e94..c97c61b 100644 --- a/test/jestMatchers.test.ts +++ b/test/jestMatchers.test.ts @@ -193,6 +193,18 @@ Calls: `); }); + it('fails on receiving less Commands than the nth requested', async () => { + const sns = new SNSClient({}); + await sns.send(publishCmd1); + + expect(() => expect(snsMock).toHaveReceivedNthCommandWith(2, PublishCommand, {Message: publishCmd2.input.Message})).toThrowErrorMatchingInlineSnapshot(` +"Expected SNSClient to receive 2. \\"PublishCommand\\" with {\\"Message\\": \\"second mock message\\"} + +Calls: + 1. PublishCommand: {\\"Message\\": \\"mock message\\", \\"TopicArn\\": \\"arn:aws:sns:us-east-1:111111111111:MyTopic\\"}" +`); + }); + it('passes on match with asymmetric matcher', async () => { const sns = new SNSClient({}); await sns.send(publishCmd1);