diff --git a/package-lock.json b/package-lock.json index 3dd2bba..131add5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/test", - "version": "4.8.0", + "version": "4.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/test", - "version": "4.8.0", + "version": "4.9.0", "license": "MIT", "dependencies": { "@japa/assert": "^1.4.1", @@ -17,7 +17,7 @@ "sinon": "^15.1.0" }, "devDependencies": { - "@athenna/common": "^4.14.0", + "@athenna/common": "^4.15.5", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "c8": "^8.0.0", @@ -74,9 +74,9 @@ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" }, "node_modules/@athenna/common": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.14.0.tgz", - "integrity": "sha512-OOe0X5rvaDyEUs5hSIs2Ggizs6koKNsmtQKN9y+ePNHITzXeFbJEAa+tGik8bgQwGFhEfQ3bpaCDEZFGFyeqCA==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/@athenna/common/-/common-4.15.5.tgz", + "integrity": "sha512-7VjL5V9QkTouQRA04uz9yDHe2TO7wDPt7vLZiDPAXEB4ctIJEDKScuXMwQunzcQs5GKw6bFbA1CPd11G/ssaTw==", "dev": true, "dependencies": { "@fastify/formbody": "^7.4.0", @@ -85,6 +85,7 @@ "chalk": "^5.3.0", "change-case": "^4.1.2", "collect.js": "^4.36.1", + "execa": "^8.0.1", "fastify": "^4.23.2", "got": "^12.6.1", "http-status-codes": "^2.2.0", @@ -136,6 +137,74 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/@athenna/common/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@athenna/common/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/@athenna/common/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@athenna/common/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -148,6 +217,36 @@ "node": ">=10" } }, + "node_modules/@athenna/common/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@athenna/common/node_modules/parent-module": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-3.0.0.tgz", @@ -163,6 +262,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@athenna/common/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@athenna/common/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@athenna/common/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", @@ -3959,9 +4094,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "engines": { "node": "*" } diff --git a/package.json b/package.json index ff50d35..1ced182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/test", - "version": "4.8.0", + "version": "4.9.0", "description": "The Athenna test runner. Built on top of Japa.", "license": "MIT", "author": "João Lenon ", @@ -58,7 +58,7 @@ "sinon": "^15.1.0" }, "devDependencies": { - "@athenna/common": "^4.14.0", + "@athenna/common": "^4.15.5", "@typescript-eslint/eslint-plugin": "^5.56.0", "@typescript-eslint/parser": "^5.56.0", "c8": "^8.0.0", diff --git a/src/globals/Assert.ts b/src/globals/Assert.ts index e23dcf4..6f8cb4e 100644 --- a/src/globals/Assert.ts +++ b/src/globals/Assert.ts @@ -44,6 +44,16 @@ Assert.macro( } ) +Assert.macro('calledWithMatch', function (mock: SinonSpy, ...args: any[]) { + const hasCalledWithMatch = mock.calledWithMatch(...args) + + if (!hasCalledWithMatch) { + return this.deepEqual(mock.args[0], args) + } + + return this.isTrue(hasCalledWithMatch) +}) + Assert.macro('calledBefore', function (mock: SinonSpy, beforeMock: SinonSpy) { return this.isTrue(mock.calledBefore(beforeMock)) }) @@ -66,6 +76,16 @@ Assert.macro('notCalledWith', function (mock: SinonSpy, ...args: any[]) { return this.isFalse(hasCalledWith) }) +Assert.macro('notCalledWithMatch', function (mock: SinonSpy, ...args: any[]) { + const hasCalledWithMatch = mock.calledWithMatch(...args) + + if (hasCalledWithMatch) { + return this.notDeepEqual(mock.args[0], args) + } + + return this.isFalse(hasCalledWithMatch) +}) + Assert.macro('calledOnceWith', function (mock: SinonSpy, ...args: any[]) { return this.isTrue(mock.calledOnceWith(...args)) }) @@ -140,6 +160,32 @@ declare module '@japa/assert' { * determined arguments. */ notCalledWith(mock: SinonSpy, ...args: any[]): void + /** + * Assert that the given mock was called with the + * arguments matching some of the given arguments. + * This is the same of doing: + * `assert.calledWith(mock, Mock.match(arg1), Mock.match(arg2))` + * + * @example + * ```ts + * console.log('hello', 'world', '!') + * assert.calledWithMatch(console.log, 'hello', 'world') // passes + * ``` + */ + calledWithMatch(mock: SinonSpy, ...args: any[]): void + /** + * Assert that the given mock was not called with the + * arguments matching some of the given arguments. + * This is the same of doing: + * `assert.notCalledWith(mock, Mock.match(arg1), Mock.match(arg2))` + * + * @example + * ```ts + * console.log('hello', 'world', '!') + * assert.notCalledWithMatch(console.log, 'hello', 'world') // fails + * ``` + */ + notCalledWithMatch(mock: SinonSpy, ...args: any[]): void /** * Assert that the given mock was called only once * with the determined arguments. diff --git a/src/mocks/Mock.ts b/src/mocks/Mock.ts index d341eb4..0308c66 100644 --- a/src/mocks/Mock.ts +++ b/src/mocks/Mock.ts @@ -7,15 +7,16 @@ * file that was distributed with this source code. */ -import { createSandbox } from 'sinon' -import { MockBuilder } from '#src/mocks/MockBuilder' import type { Spy, + Match, SpyMethod, StubMethod, SpyInstance, StubInstance } from '#src' +import { createSandbox } from 'sinon' +import { MockBuilder } from '#src/mocks/MockBuilder' export class Mock { /** @@ -69,6 +70,18 @@ export class Mock { return Mock.sandbox.fake() } + /** + * Create a matcher for the given value. + * + * @example + * ```ts + * assert.isTrue({ hello: 'world', name: 'João' }, Mock.match({ hello: 'world' })) + * ``` + */ + public static match(value: any): Match { + return Mock.sandbox.match(value) + } + /** * Restore all mocks to default. */ diff --git a/src/types/Match.ts b/src/types/Match.ts new file mode 100644 index 0000000..c3deca0 --- /dev/null +++ b/src/types/Match.ts @@ -0,0 +1,12 @@ +/** + * @athenna/test + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { SinonMatcher } from 'sinon' + +export type Match = SinonMatcher diff --git a/src/types/index.ts b/src/types/index.ts index 4f1f0c4..e8327ba 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,7 @@ export type { Config, PluginFn } from '@japa/runner' export * from './Spy.js' +export * from './Match.js' export * from './SpyMethod.js' export * from './SpyInstance.js' export * from './Stub.js' diff --git a/tests/unit/mocks/AssertMockTest.ts b/tests/unit/mocks/AssertMockTest.ts index e675c6a..dccbf40 100644 --- a/tests/unit/mocks/AssertMockTest.ts +++ b/tests/unit/mocks/AssertMockTest.ts @@ -66,6 +66,17 @@ export default class AssertMockTest { assert.calledWith(spy, 1) } + @Test() + public async shouldBeAbleToAssertSpyWasCalledWithMatchingArgs({ assert }: Context) { + const userService = new UserService() + + const spy = Mock.spy(userService, 'findById') + + await userService.findById(1) + + assert.calledWithMatch(spy, 1) + } + @Test() public async shouldBeAbleToAssertSpyWasCalledNotWithArgs({ assert }: Context) { const userService = new UserService() @@ -78,6 +89,18 @@ export default class AssertMockTest { assert.notCalledWith(spy, 2) } + @Test() + public async shouldBeAbleToAssertSpyWasCalledNotWithMatchingArgs({ assert }: Context) { + const userService = new UserService() + + const spy = Mock.spy(userService, 'findById') + + await userService.findById(1) + + assert.calledWithMatch(spy, 1) + assert.notCalledWithMatch(spy, 2) + } + @Test() @Fails() public async shouldFailWhenAssertingThatSpyWasNotCalledWithArgs({ assert }: Context) { @@ -90,6 +113,18 @@ export default class AssertMockTest { assert.calledWith(spy, 2) } + @Test() + @Fails() + public async shouldFailWhenAssertingThatSpyWasNotCalledWithMatchingArgs({ assert }: Context) { + const userService = new UserService() + + const spy = Mock.spy(userService, 'findById') + + await userService.findById(1) + + assert.calledWithMatch(spy, 2) + } + @Test() @Fails() public async shouldFailWhenAssertingThatSpyWasCalledWithArgs({ assert }: Context) { @@ -102,6 +137,18 @@ export default class AssertMockTest { assert.notCalledWith(spy, 1) } + @Test() + @Fails() + public async shouldFailWhenAssertingThatSpyWasCalledWithMatchingArgs({ assert }: Context) { + const userService = new UserService() + + const spy = Mock.spy(userService, 'findById') + + await userService.findById(1) + + assert.notCalledWithMatch(spy, 1) + } + @Test() public async shouldBeAbleToAssertSpyWasCalledOnceWithArgs({ assert }: Context) { const userService = new UserService()