diff --git a/docs/snippets/package.json b/docs/snippets/package.json index 345cfb3397..5f8fe345b3 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -32,6 +32,8 @@ "@aws-sdk/client-secrets-manager": "^3.250.0", "@aws-sdk/client-ssm": "^3.245.0", "@aws-sdk/util-dynamodb": "^3.245.0", + "aws-sdk-client-mock": "^2.0.1", + "aws-sdk-client-mock-jest": "^2.0.1", "axios": "^1.2.4" } -} \ No newline at end of file +} diff --git a/docs/snippets/parameters/testingYourCodeClearCache.ts b/docs/snippets/parameters/testingYourCodeClearCache.ts new file mode 100644 index 0000000000..a70e113324 --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeClearCache.ts @@ -0,0 +1,15 @@ +import { clearCaches } from '@aws-lambda-powertools/parameters'; + +describe('Function tests', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + clearCaches(); + }); + + // ... + +}); \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeClientHandler.ts b/docs/snippets/parameters/testingYourCodeClientHandler.ts new file mode 100644 index 0000000000..dce84b04c1 --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeClientHandler.ts @@ -0,0 +1,15 @@ +import { getSecret } from '@aws-lambda-powertools/parameters/secrets'; + +export const handler = async (_event: unknown, _context: unknown): Promise> => { + try { + const parameter = await getSecret('my-secret'); + + return { + value: parameter + }; + } catch (error) { + return { + message: 'Unable to retrieve secret', + }; + } +}; \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeClientJestMock.ts b/docs/snippets/parameters/testingYourCodeClientJestMock.ts new file mode 100644 index 0000000000..743488d72b --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeClientJestMock.ts @@ -0,0 +1,41 @@ +import { handler } from './testingYourCodeFunctionsHandler'; +import { + SecretsManagerClient, + GetSecretValueCommand, + ResourceNotFoundException, +} from '@aws-sdk/client-secrets-manager'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; + +describe('Function tests', () => { + + const client = mockClient(SecretsManagerClient); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + client.reset(); + }); + + test('it returns the correct error message', async () => { + + // Prepare + client.on(GetSecretValueCommand) + .rejectsOnce(new ResourceNotFoundException({ + $metadata: { + httpStatusCode: 404, + }, + message: 'Unable to retrieve secret', + })); + + // Act + const result = await handler({}, {}); + + // Assess + expect(result).toStrictEqual({ message: 'Unable to retrieve secret' }); + + }); + +}); \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeFunctionsHandler.ts b/docs/snippets/parameters/testingYourCodeFunctionsHandler.ts new file mode 100644 index 0000000000..bb440aef19 --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeFunctionsHandler.ts @@ -0,0 +1,9 @@ +import { getParameter } from '@aws-lambda-powertools/parameters/ssm'; + +export const handler = async (_event: unknown, _context: unknown): Promise> => { + const parameter = await getParameter('my/param'); + + return { + value: parameter + }; +}; \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeFunctionsJestMock.ts b/docs/snippets/parameters/testingYourCodeFunctionsJestMock.ts new file mode 100644 index 0000000000..de474fb15c --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeFunctionsJestMock.ts @@ -0,0 +1,30 @@ +import { handler } from './testingYourCodeFunctionsHandler'; +import { getParameter } from '@aws-lambda-powertools/parameters/ssm'; + +jest.mock('@aws-lambda-powertools/parameters/ssm', () => ({ + getParameter: jest.fn() +})); +const mockedGetParameter = getParameter as jest.MockedFunction; + +describe('Function tests', () => { + + beforeEach(() => { + mockedGetParameter.mockClear(); + }); + + test('it returns the correct response', async () => { + + // Prepare + mockedGetParameter.mockResolvedValue('my/param'); + + // Act + const result = await handler({}, {}); + + // Assess + expect(result).toEqual({ + value: 'my/param', + }); + + }); + +}); \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeProvidersHandler.ts b/docs/snippets/parameters/testingYourCodeProvidersHandler.ts new file mode 100644 index 0000000000..d733030381 --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeProvidersHandler.ts @@ -0,0 +1,14 @@ +import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; + +const provider = new AppConfigProvider({ + environment: 'dev', + application: 'my-app', +}); + +export const handler = async (_event: unknown, _context: unknown): Promise> => { + const config = await provider.get('my-config'); + + return { + value: config + }; +}; \ No newline at end of file diff --git a/docs/snippets/parameters/testingYourCodeProvidersJestMock.ts b/docs/snippets/parameters/testingYourCodeProvidersJestMock.ts new file mode 100644 index 0000000000..cdaa0d5eae --- /dev/null +++ b/docs/snippets/parameters/testingYourCodeProvidersJestMock.ts @@ -0,0 +1,33 @@ +import { handler } from './testingYourCodeFunctionsHandler'; +import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; + +describe('Function tests', () => { + + const providerSpy = jest.spyOn(AppConfigProvider.prototype, 'get'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it retrieves the config once and uses the correct name', async () => { + + // Prepare + const expectedConfig = { + feature: { + enabled: true, + name: 'paywall', + }, + }; + providerSpy.mockResolvedValueOnce(expectedConfig); + + // Act + const result = await handler({}, {}); + + // Assess + expect(result).toStrictEqual({ value: expectedConfig }); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('my-config'); + + }); + +}); \ No newline at end of file diff --git a/docs/utilities/parameters.md b/docs/utilities/parameters.md index 6a73ad7afd..6409149cf5 100644 --- a/docs/utilities/parameters.md +++ b/docs/utilities/parameters.md @@ -134,7 +134,7 @@ By default, the provider will cache parameters retrieved in-memory for 5 seconds You can adjust how long values should be kept in cache by using the param `maxAge`, when using `get()` or `getMultiple()` methods across all providers. -```typescript hl_lines="7 10" title="Caching parameters values in memory for longer than 5 seconds" +```typescript hl_lines="7 11" title="Caching parameters values in memory for longer than 5 seconds" --8<-- "docs/snippets/parameters/adjustingCacheTTL.ts" ``` @@ -166,7 +166,7 @@ The AWS Systems Manager Parameter Store provider supports two additional argumen | **decrypt** | `false` | Will automatically decrypt the parameter (see required [IAM Permissions](#iam-permissions)). | | **recursive** | `true` | For `getMultiple()` only, will fetch all parameter values recursively based on a path prefix. | -```typescript hl_lines="6 8" title="Example with get() and getMultiple()" +```typescript hl_lines="6 9" title="Example with get() and getMultiple()" --8<-- "docs/snippets/parameters/ssmProviderDecryptAndRecursive.ts" ``` @@ -397,3 +397,56 @@ The **`clientConfig`** parameter enables you to pass in a custom [config object] ```typescript hl_lines="2 4-5" --8<-- "docs/snippets/parameters/clientConfig.ts" ``` + +## Testing your code + +### Mocking parameter values + +For unit testing your applications, you can mock the calls to the parameters utility to avoid calling AWS APIs. This can be achieved in a number of ways - in this example, we use [Jest mock functions](https://jestjs.io/docs/es6-class-mocks#the-4-ways-to-create-an-es6-class-mock) to patch the `getParameters` function. + +=== "handler.test.ts" + ```typescript hl_lines="2 4-7 12 18" + --8<-- "docs/snippets/parameters/testingYourCodeFunctionsJestMock.ts" + ``` + +=== "handler.ts" + ```typescript + --8<-- "docs/snippets/parameters/testingYourCodeFunctionsHandler.ts" + ``` + +With this pattern in place, you can customize the return values of the mocked function to test different scenarios without calling AWS APIs. + +A similar pattern can be applied also to any of the built-in provider classes - in this other example, we use [Jest spyOn method](https://jestjs.io/docs/es6-class-mocks#mocking-a-specific-method-of-a-class) to patch the `get` function of the `AppConfigProvider` class. This is useful also when you want to test that the correct arguments are being passed to the Parameters utility. + +=== "handler.test.ts" + ```typescript hl_lines="2 6 9 21" + --8<-- "docs/snippets/parameters/testingYourCodeProvidersJestMock.ts" + ``` + +=== "handler.ts" + ```typescript + --8<-- "docs/snippets/parameters/testingYourCodeProvidersHandler.ts" + ``` + +In some other cases, you might want to mock the AWS SDK v3 client itself, in these cases we recommend using the [`aws-sdk-client-mock`](https://www.npmjs.com/package/aws-sdk-client-mock) and [`aws-sdk-client-mock-jest`](https://www.npmjs.com/package/aws-sdk-client-mock-jest) libraries. This is useful when you want to test how your code behaves when the AWS SDK v3 client throws an error or a specific response. + +=== "handler.test.ts" + ```typescript hl_lines="2-8 12 19 25-31" + --8<-- "docs/snippets/parameters/testingYourCodeClientJestMock.ts" + ``` + +=== "handler.ts" + ```typescript + --8<-- "docs/snippets/parameters/testingYourCodeClientHandler.ts" + ``` + +### Clearing cache + +Parameters utility caches all parameter values for performance and cost reasons. However, this can have unintended interference in tests using the same parameter name. + +Within your tests, you can use `clearCache` method available in [every provider](#built-in-provider-class). When using multiple providers or higher level functions like `getParameter`, use the `clearCaches` standalone function to clear cache globally. + +=== "handler.test.ts" + ```typescript hl_lines="1 9-11" + --8<-- "docs/snippets/parameters/testingYourCodeClearCache.ts" + ``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c21c3e3d9b..14da5b820d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,8 @@ "@aws-sdk/client-secrets-manager": "^3.250.0", "@aws-sdk/client-ssm": "^3.245.0", "@aws-sdk/util-dynamodb": "^3.245.0", + "aws-sdk-client-mock": "^2.0.1", + "aws-sdk-client-mock-jest": "^2.0.1", "axios": "^1.2.4" } }, @@ -86,7 +88,7 @@ } }, "layers": { - "version": "1.6.0", + "version": "1.7.0", "license": "MIT-0", "bin": { "layer": "bin/layers.js" @@ -16717,7 +16719,7 @@ }, "packages/commons": { "name": "@aws-lambda-powertools/commons", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT-0" }, "packages/idempotency": { @@ -16735,10 +16737,10 @@ }, "packages/logger": { "name": "@aws-lambda-powertools/logger", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "dependencies": { - "@aws-lambda-powertools/commons": "^1.6.0", + "@aws-lambda-powertools/commons": "^1.7.0", "lodash.merge": "^4.6.2" }, "devDependencies": { @@ -16747,10 +16749,10 @@ }, "packages/metrics": { "name": "@aws-lambda-powertools/metrics", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^1.6.0" + "@aws-lambda-powertools/commons": "^1.7.0" }, "devDependencies": { "@types/promise-retry": "^1.1.3", @@ -16759,7 +16761,7 @@ }, "packages/parameters": { "name": "@aws-lambda-powertools/parameters", - "version": "1.5.0", + "version": "1.7.0", "license": "MIT-0", "dependencies": { "@aws-sdk/util-base64-node": "^3.209.0" @@ -16788,10 +16790,10 @@ }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^1.6.0", + "@aws-lambda-powertools/commons": "^1.7.0", "aws-xray-sdk-core": "^3.4.1" }, "devDependencies": { @@ -17037,7 +17039,7 @@ "@aws-lambda-powertools/logger": { "version": "file:packages/logger", "requires": { - "@aws-lambda-powertools/commons": "^1.6.0", + "@aws-lambda-powertools/commons": "^1.7.0", "@types/lodash.merge": "^4.6.7", "lodash.merge": "^4.6.2" } @@ -17045,7 +17047,7 @@ "@aws-lambda-powertools/metrics": { "version": "file:packages/metrics", "requires": { - "@aws-lambda-powertools/commons": "^1.6.0", + "@aws-lambda-powertools/commons": "^1.7.0", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" } @@ -17077,11 +17079,11 @@ "@aws-lambda-powertools/tracer": { "version": "file:packages/tracer", "requires": { - "@aws-lambda-powertools/commons": "^1.6.0", + "@aws-lambda-powertools/commons": "^1.7.0", "@aws-sdk/client-dynamodb": "^3.231.0", "@types/promise-retry": "^1.1.3", "aws-sdk": "^2.1276.0", - "aws-xray-sdk-core": "3.4.1", + "aws-xray-sdk-core": "^3.4.1", "axios": "^1.2.1", "promise-retry": "^2.0.1" } @@ -23238,6 +23240,8 @@ "@aws-sdk/client-secrets-manager": "^3.250.0", "@aws-sdk/client-ssm": "^3.245.0", "@aws-sdk/util-dynamodb": "^3.245.0", + "aws-sdk-client-mock": "2.0.1", + "aws-sdk-client-mock-jest": "2.0.1", "axios": "^1.2.4" }, "dependencies": {