Skip to content
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(jest): add support for vitest #231

Merged
merged 12 commits into from
Oct 1, 2024
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
**/test-d/**
test-e2e/**
**/dist/**

**/vitest.config.ts
**/vitest.serializer.ts
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ In action:
- [About AWS SDK v3](#about-aws-sdk-v3)
- [Usage](#usage)
- [Install](#install)
- [Versions compatibility](#versions-compatibility)
- [Import](#import)
- [Mock](#mock)
- [DynamoDB DocumentClient](#dynamodb-documentclient)
Expand All @@ -38,14 +39,17 @@ In action:
- [SDK v2-style mocks](#sdk-v2-style-mocks)
- [Inspect](#inspect)
- [Reset and restore](#reset-and-restore)
- [Jest matchers](#jest-matchers)
- [Custom matchers](#custom-matchers)
- [Jest](#jest)
- [Vitest](#vitest)
- [API Reference](#api-reference)
- [AWS Lambda example](#aws-lambda-example)
- [Caveats](#caveats)
- [Mixed @smithy/types versions](#mixed-smithytypes-versions)
- [AwsClientStub and strictFunctionTypes](#awsclientstub-and-strictfunctiontypes)
- [Order of mock behaviors](#order-of-mock-behaviors)
- [Order of type and instance mocks](#order-of-type-and-instance-mocks)
- [Using with Mocha](#using-with-mocha)

## About AWS SDK v3

Expand Down Expand Up @@ -363,7 +367,7 @@ s3Mock.on(UploadPartCommand).rejects();
#### S3 GetObjectCommand

AWS SDK wraps the stream in the S3 `GetObjectCommand` result to provide utility methods to parse it.
To mock it, you need to install the [`@smithy/util-stream`](https://www.npmjs.com/package/@smithy/util-stream) package
To mock it, you need to install the [`@smithy/util-stream`](https://www.npmjs.com/package/@smithy/util-stream) package
and call the wrapping function `sdkStreamMixin()` on the stream you provide as the command output:

```ts
Expand Down Expand Up @@ -510,7 +514,9 @@ You can also pass custom [Sinon Sandbox](https://sinonjs.org/releases/latest/san
with `mockClient(client, { sandbox: mySandbox })`
to manage all mocks lifecycle at once.

### Jest matchers
### Custom matchers

#### Jest

Custom [Jest](https://jestjs.io/) matchers simplify verification
that the mocked Client was called with given Commands.
Expand Down Expand Up @@ -556,10 +562,19 @@ expect(snsMock).toHaveReceivedNthSpecificCommandWith(
);
```

Shorter aliases exist, like `toReceiveCommandTimes()`.
Shorter aliases exist, like `toReceiveCommandTimes()`.

#### Vitest

Use those matchers with [Vitest](https://vitest.dev/):

```ts
import 'aws-sdk-client-mock-jest/vitest';
import { expect } from 'vitest';

To use those matchers with [Vitest](https://vitest.dev/), set `test.globals` to `true` in `vite.config.js`
(see [#139](https://github.com/m-radzikowski/aws-sdk-client-mock/issues/139)).
// a PublishCommand was sent to SNS
expect(snsMock).toHaveReceivedCommand(PublishCommand);
```

To use the matchers outside of Jest, you can pull in the [expect](https://www.npmjs.com/package/expect) library separately
and add it to the global scope directly, e.g.:
Expand Down
47 changes: 41 additions & 6 deletions packages/aws-sdk-client-mock-jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,46 @@
"jest-matchers"
],
"scripts": {
"test": "jest --coverage --colors",
"test": "pnpm run jest && pnpm run vitest",
"jest": "jest --coverage --colors ",
"vitest": "vitest run",
"test-types": "tsd",
"build:cjs": "tsc -p tsconfig.json",
"build:es": "tsc -p tsconfig.es.json",
"prebuild": "rimraf dist/",
"build": "pnpm run build:cjs && pnpm run build:es",
"local-publish": "pnpm publish --registry http://localhost:4873/ --no-git-checks"
},
"module": "dist/es/index.js",
"main": "dist/cjs/index.js",
"types": "dist/types/index.d.ts",
"module": "dist/es/jest.js",
"main": "dist/cjs/jest.js",
"types": "dist/types/jest.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/types/jest.d.ts",
"default": "./dist/cjs/jest.js"
},
"import": {
"types": "./dist/types/jest.d.ts",
"default": "./dist/es/jest.js"
}
},
"./vitest": {
"require": {
"types": "./dist/types/vitest.d.ts",
"default": "./dist/cjs/vitest.js"
},
"import": {
"types": "./dist/types/vitest.d.ts",
"default": "./dist/es/vitest.js"
}
}
},
"files": [
"dist"
],
"dependencies": {
"@vitest/expect": ">1.6.0",
"expect": ">28.1.3",
"tslib": "^2.1.0"
},
Expand All @@ -49,12 +74,22 @@
"@smithy/types": "1.1.0",
"@types/jest": "29.5.12",
"@types/sinon": "^17.0.3",
"@vitest/coverage-v8": "^2.1.1",
"aws-sdk-client-mock": "workspace:*",
"chalk": "^5.3.0",
"expect": "29.7.0",
"jest-serializer-ansi-escapes": "3.0.0"
"jest-serializer-ansi-escapes": "3.0.0",
"pretty-ansi": "^2.0.0",
"vitest": "^2.1.1"
},
"peerDependencies": {
"aws-sdk-client-mock": "workspace:*"
"aws-sdk-client-mock": "workspace:*",
"vitest": ">1.6.0"
},
"peerDependenciesMeta": {
"vitest": {
"optional": true
}
},
"jest": {
"preset": "ts-jest",
Expand Down
1 change: 0 additions & 1 deletion packages/aws-sdk-client-mock-jest/src/index.ts

This file was deleted.

137 changes: 137 additions & 0 deletions packages/aws-sdk-client-mock-jest/src/jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import type { MatcherContext } from 'expect';
import { expect } from 'expect';
import type { AwsSdkMockMatchers } from './jestMatchers';
import { createBaseMatchers } from './jestMatchers';
import type {
AnySpyCall,
AwsSdkMockAliasMatchers,
CommonMatcherUtils,
MatcherFunction,
} from './types';

/**
* Prettyprints command calls for message
*/
function addCalls(
ctxUtils: CommonMatcherUtils,
calls: AnySpyCall[],
...msgs: string[]
) {
if (calls.length === 0) return msgs.join('\n');

return [
...msgs,
'',
'Calls:',
...calls.map(
(c, i) =>
` ${i + 1}. ${c.args[0].constructor.name}: ${ctxUtils.printReceived(
c.args[0].input
)}`
),
].join('\n');
}

const baseMatchers = createBaseMatchers<MatcherContext['utils']>({
toHaveReceivedCommand: ({
client,
cmd,
notPrefix,
calls,
commandCalls,
ctxUtils,
}) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive ${ctxUtils.printExpected(cmd)}`,
`${client} received ${ctxUtils.printExpected(cmd)} ${ctxUtils.printReceived(commandCalls.length)} times`
),
toHaveReceivedCommandTimes:
(expectedCalls) =>
({ calls, client, cmd, commandCalls, notPrefix, ctxUtils }) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive ${ctxUtils.printExpected(cmd)} ${ctxUtils.printExpected(expectedCalls)} times`,
`${client} received ${ctxUtils.printExpected(cmd)} ${ctxUtils.printReceived(commandCalls.length)} times`
),

toHaveReceivedCommandWith:
(input) =>
({ client, cmd, notPrefix, data, calls, ctxUtils }) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive ${ctxUtils.printExpected(cmd)} with ${ctxUtils.printExpected(input)}`,
`${client} received matching ${ctxUtils.printExpected(cmd)} ${ctxUtils.printReceived(data.matchCount)} times`
),

toHaveReceivedNthCommandWith:
(call, input) =>
({ cmd, client, data, notPrefix, ctxUtils, calls }) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive ${call}. ${ctxUtils.printExpected(cmd)} with ${ctxUtils.printExpected(input)}`,
...(data.received
? [
`${client} received ${ctxUtils.printReceived(data.received.constructor.name)} with input:`,
ctxUtils.printDiffOrStringify(input, data.received.input, 'Expected', 'Received', false),
]
: [])
),
toHaveReceivedNthSpecificCommandWith:
(call, input) =>
({ cmd, client, data, notPrefix, ctxUtils, calls }) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive ${call}. ${ctxUtils.printExpected(cmd)} with ${ctxUtils.printExpected(input)}`,
...(data.received
? [
`${client} received ${ctxUtils.printReceived(data.received.constructor.name)} with input:`,
ctxUtils.printDiffOrStringify(input, data.received.input, 'Expected', 'Received', false),
]
: [])
),
toHaveReceivedAnyCommand: ({ client, notPrefix, calls, ctxUtils }) =>
addCalls(
ctxUtils,
calls,
`Expected ${client} to ${notPrefix}receive any command`,
`${client} received any command ${ctxUtils.printReceived(calls.length)} times`
),
},
(sample: Record<string, unknown>) => expect.objectContaining(sample)
);

/* typing ensures keys matching */
const aliasMatchers: {
[P in keyof AwsSdkMockAliasMatchers<unknown>]: MatcherFunction<MatcherContext['utils']>;
} = {
toReceiveCommandTimes: baseMatchers.toHaveReceivedCommandTimes,
toReceiveCommand: baseMatchers.toHaveReceivedCommand,
toReceiveCommandWith: baseMatchers.toHaveReceivedCommandWith,
toReceiveNthCommandWith: baseMatchers.toHaveReceivedNthCommandWith,
toReceiveNthSpecificCommandWith:baseMatchers.toHaveReceivedNthSpecificCommandWith,
toReceiveAnyCommand: baseMatchers.toHaveReceivedAnyCommand,
};

// Skip registration if jest expect does not exist
if (typeof expect !== 'undefined' && typeof expect.extend === 'function') {
expect.extend({ ...baseMatchers, ...aliasMatchers });
}

/**
* Types for @types/jest
*/
declare global {
namespace jest {
interface Matchers<R = void> extends AwsSdkMockMatchers<R> {}
}
}
declare module 'expect' {
interface Matchers<R = void> extends AwsSdkMockMatchers<R> {}
}
Loading