Skip to content

Commit

Permalink
feat(jest): add support for vitest (#231)
Browse files Browse the repository at this point in the history
  • Loading branch information
YiCChi authored Oct 1, 2024
1 parent 72ebb74 commit 3ed37a9
Show file tree
Hide file tree
Showing 15 changed files with 2,447 additions and 519 deletions.
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

0 comments on commit 3ed37a9

Please sign in to comment.