Skip to content

Commit

Permalink
feat: getting mock calls with command type and payload filter (#61)
Browse files Browse the repository at this point in the history
* feat: method to get mock calls of only specified command

* feat: getting mock calls of specified command with input matcher

* docs: getting calls of a specified command readme and jsdoc
  • Loading branch information
m-radzikowski authored Nov 6, 2021
1 parent 7c85252 commit b3f3250
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 20 deletions.
44 changes: 30 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ In action:
- [DynamoDB DocumentClient](#dynamodb-documentclient)
- [Lib Storage Upload](#lib-storage-upload)
- [Paginated operations](#paginated-operations)
- [Inspect](#inspect)
- [API Reference](#api-reference)
- [AWS Lambda example](#aws-lambda-example)
- [Caveats](#caveats)
Expand Down Expand Up @@ -212,20 +213,6 @@ snsMock
});
```

Inspect received calls:

```typescript
snsMock.calls(); // all received calls
snsMock.call(0); // first received call
```

Under the hood, the library uses [Sinon.js](https://sinonjs.org/) `stub`.
You can get the stub instance to configure and use it directly:

```typescript
const snsSendStub = snsMock.send;
```

#### DynamoDB DocumentClient

You can mock the `DynamoDBDocumentClient` just like any other Client:
Expand Down Expand Up @@ -299,6 +286,35 @@ for await (const page of paginator) {
}
```

### Inspect

Inspect received calls:

```typescript
snsMock.calls(); // all received calls
snsMock.call(0); // first received call
```

Get calls of a specified command:

```typescript
snsMock.commandCalls(PublishCommand)
```

Get calls of a specified command with given payload
(you can force strict matching by passing third param `strict: true`):

```typescript
snsMock.commandCalls(PublishCommand, {Message: 'My message'})
```

Under the hood, the library uses [Sinon.js](https://sinonjs.org/) `stub`.
You can get the stub instance to configure and use it directly:

```typescript
const snsSendStub = snsMock.send;
```

## API Reference

See the [full API Reference](https://m-radzikowski.github.io/aws-sdk-client-mock/).
Expand Down
30 changes: 26 additions & 4 deletions src/awsClientStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export class AwsStub<TInput extends object, TOutput extends MetadataBearer> impl
*
* Install `@types/sinon` for TypeScript typings.
*/
public send: SinonStub<[AwsCommand<TInput, TOutput>], unknown>;
public send: SinonStub<[AwsCommand<TInput, TOutput>], Promise<TOutput>>;

private readonly anyCommandBehavior: CommandBehavior<TInput, TOutput, TOutput>;

constructor(send: SinonStub<[AwsCommand<TInput, TOutput>], unknown>) {
constructor(send: SinonStub<[AwsCommand<TInput, TOutput>], Promise<TOutput>>) {
this.send = send;
this.anyCommandBehavior = new CommandBehavior(this, send);
}
Expand Down Expand Up @@ -78,17 +78,39 @@ export class AwsStub<TInput extends object, TOutput extends MetadataBearer> impl
* Returns recorded calls to the stub.
* Clear history with {@link resetHistory} or {@link reset}.
*/
calls(): SinonSpyCall<[AwsCommand<TInput, TOutput>], unknown>[] {
calls(): SinonSpyCall<[AwsCommand<TInput, TOutput>], Promise<TOutput>>[] {
return this.send.getCalls();
}

/**
* Returns n-th recorded call to the stub.
*/
call(n: number): SinonSpyCall<[AwsCommand<TInput, TOutput>], unknown> {
call(n: number): SinonSpyCall<[AwsCommand<TInput, TOutput>], Promise<TOutput>> {
return this.send.getCall(n);
}

/**
* Returns recorded calls of given Command only.
* @param commandType Command type to match
* @param input Command payload to match
* @param strict Should the payload match strictly (default false, will match if all defined payload properties match)
*/
commandCalls<TCmd extends AwsCommand<any, any>,
TCmdInput extends TCmd extends AwsCommand<infer TIn, any> ? TIn : never,
TCmdOutput extends TCmd extends AwsCommand<any, infer TOut> ? TOut : never,
>(
commandType: new (input: TCmdInput) => TCmd,
input?: Partial<TCmdInput>,
strict?: boolean,
): SinonSpyCall<[TCmd], Promise<TCmdOutput>>[] {
return this.send.getCalls()
.filter((call): call is SinonSpyCall<[TCmd], Promise<TCmdOutput>> => {
const isProperType = call.args[0] instanceof commandType;
const inputMatches = this.createInputMatcher(input, strict).test(call.args[0]);
return isProperType && inputMatches;
});
}

/**
* Allows specifying the behavior for a given Command type and its input (parameters).
*
Expand Down
2 changes: 1 addition & 1 deletion src/mockClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const mockClient = <TInput extends object, TOutput extends MetadataBearer
send.restore();
}

const sendStub: SinonStub<[Command<TInput, any, TOutput, any, any>], unknown> = stub(instance, 'send');
const sendStub = stub(instance, 'send') as SinonStub<[Command<TInput, any, TOutput, any, any>], Promise<TOutput>>;

return new AwsStub<TInput, TOutput>(sendStub);
};
Expand Down
6 changes: 5 additions & 1 deletion test-d/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AwsClientStub, mockClient} from '../src';
import {ListTopicsCommand, PublishCommand, SNSClient} from '@aws-sdk/client-sns';
import {ListTopicsCommand, PublishCommand, PublishCommandOutput, SNSClient} from '@aws-sdk/client-sns';
import {expectError, expectType} from 'tsd';

expectType<AwsClientStub<SNSClient>>(mockClient(SNSClient));
Expand All @@ -26,3 +26,7 @@ expectError(mockClient(SNSClient).on(ListTopicsCommand, {TopicArn: '', Message:
// invalid output types
expectError(mockClient(SNSClient).on(PublishCommand).resolves({MessageId: '', Topics: []}));
expectError(mockClient(SNSClient).on(PublishCommand).resolves({Topics: []}));

// Sinon Spy
expectType<PublishCommand>(mockClient(SNSClient).commandCalls(PublishCommand)[0].args[0]);
expectType<Promise<PublishCommandOutput>>(mockClient(SNSClient).commandCalls(PublishCommand)[0].returnValue);
35 changes: 35 additions & 0 deletions test/mockClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,41 @@ describe('spying on the mock', () => {
expect(snsMock.call(1).args[0].input).toStrictEqual(publishCmd2.input);
});

it('finds calls of given command type', async () => {
snsMock.resolves({
MessageId: uuid1,
});

const sns = new SNSClient({});
await sns.send(publishCmd1);
await sns.send(new ListTopicsCommand({}));

expect(snsMock.calls()).toHaveLength(2);
expect(snsMock.commandCalls(PublishCommand)).toHaveLength(1);

expect(snsMock.commandCalls(PublishCommand)[0].args[0].input).toStrictEqual(publishCmd1.input);
expect(snsMock.commandCalls(PublishCommand)[0].returnValue).toStrictEqual(Promise.resolve({MessageId: uuid1}));
});

it('finds calls of given command type and input parameters', async () => {
const sns = new SNSClient({});
await sns.send(publishCmd1);
await sns.send(publishCmd2);

expect(snsMock.commandCalls(PublishCommand)).toHaveLength(2);
expect(snsMock.commandCalls(PublishCommand, {Message: publishCmd1.input.Message})).toHaveLength(1);
});

it('finds calls of given command type and exact input', async () => {
const sns = new SNSClient({});
await sns.send(publishCmd1);
await sns.send(publishCmd2);

expect(snsMock.commandCalls(PublishCommand)).toHaveLength(2);
expect(snsMock.commandCalls(PublishCommand, {Message: publishCmd1.input.Message}, true)).toHaveLength(0);
expect(snsMock.commandCalls(PublishCommand, {...publishCmd1.input}, true)).toHaveLength(1);
});

it('resets calls history', async () => {
const sns = new SNSClient({});
await sns.send(publishCmd1);
Expand Down

0 comments on commit b3f3250

Please sign in to comment.