Skip to content

Commit

Permalink
Add an example of unit testing with Jest (pulumi#1233)
Browse files Browse the repository at this point in the history
* Add an example of unit testing with Jest
* Update testing-unit-ts-mocks-jest/README.md
* Un-export the untested

Co-authored-by: Laura Santamaria <[email protected]>
  • Loading branch information
cnunciato and nimbinatus authored Jun 17, 2022
1 parent df13f6d commit 69c9a72
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 0 deletions.
2 changes: 2 additions & 0 deletions testing-unit-ts-mocks-jest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/bin/
/node_modules/
8 changes: 8 additions & 0 deletions testing-unit-ts-mocks-jest/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: testing-unit-ts-mocks-jest
runtime: nodejs
description: An example of using Jest with Pulumi to write test-driven code that mocks AWS infrastructure.
template:
config:
aws:region:
description: The AWS region to deploy into
default: us-east-2
49 changes: 49 additions & 0 deletions testing-unit-ts-mocks-jest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Unit Testing AWS Infrastructure with Jest

An example of using [Pulumi](https://pulumi.com/) with [Jest](https://jestjs.io/), the JavaScript testing framework, to write in-memory unit tests that mock AWS infrastructure. The program under test deploys a single [AWS Lambda function](https://aws.amazon.com/lambda/) and an associated [Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html).

[![Deploy with Pulumi](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/tree/master/testing-unit-ts-mocks-jest)

## Prerequisites

1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/).
1. [Install Node.js](https://www.pulumi.com/docs/intro/languages/javascript/).
1. Configure your [AWS credentials](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/).

### Deploying the App

1. Clone this repo, change to this directory, then create a new [stack](https://www.pulumi.com/docs/intro/concepts/stack/) for the project:

```bash
pulumi stack init
```

1. Specify an AWS region to deploy into:

```bash
pulumi config set aws:region us-west-2
```

1. Install Node dependencies and run the tests:

```bash
npm install
npm test
```

In a few moments, the tests should pass.

1. If you'd like to deploy the program as well, run `pulumi up`. In a few moments, the `FunctionUrl` of the `timeURL` Lambda will be emitted as a Pulumi [stack output](https://www.pulumi.com/docs/intro/concepts/stack/#outputs) called `audioURL`:
```bash
...
Outputs:
audioURL: "https://o3vbc73qd2vxrhtaao5v53yeaa0sricr.lambda-url.us-west-2.on.aws/"
```
1. When you're ready, destroy your stack and remove it:

```bash
pulumi destroy --yes
pulumi stack rm --yes
```
70 changes: 70 additions & 0 deletions testing-unit-ts-mocks-jest/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2016-2022, Pulumi Corporation. All rights reserved.

import * as pulumi from "@pulumi/pulumi";
import "jest";

function promiseOf<T>(output: pulumi.Output<T>): Promise<T> {
return new Promise(resolve => output.apply(resolve));
}

describe("My speaking clock", () => {

// Define the infra variable as a type whose shape matches the that of the
// to-be-defined resources module.
// https://www.typescriptlang.org/docs/handbook/2/typeof-types.html
let infra: typeof import("./resources");

beforeAll(() => {

// Put Pulumi in unit-test mode, mocking all calls to cloud-provider APIs.
pulumi.runtime.setMocks({

// Mock calls to create new resources and return a canned response.
newResource: (args: pulumi.runtime.MockResourceArgs): {id: string, state: any} => {

// Here, we're returning a same-shaped object for all resource types.
// We could, however, use the arguments passed into this function to
// customize the mocked-out properties of a particular resource.
// See the unit-testing docs for details:
// https://www.pulumi.com/docs/guides/testing/unit
return {
id: `${args.name}-id`,
state: args.inputs,
};
},

// Mock function calls and return whatever input properties were provided.
call: (args: pulumi.runtime.MockCallArgs) => {
return args.inputs;
},
});
});

beforeEach(async () => {

// Dynamically import the resources module.
infra = await import("./resources");
});

describe("function URL", () => {

it("is publicly accessible", async () => {
const authType = await promiseOf(infra.timeURL.authorizationType);
expect(authType).toBe("NONE");
});

it("is CORS-friendly", async () => {
const authType = await promiseOf(infra.timeURL.cors);
expect(authType).toEqual({
allowOrigins: ["*"],
allowMethods: ["GET"],
});
});

it("is bound to the right Lambda function", async () => {
const timeFuncName = await promiseOf(infra.timeFunction.name);
const timeURLFunc = await promiseOf(infra.timeURL.functionName);
expect(timeFuncName).toEqual(timeURLFunc);
});
});
});
5 changes: 5 additions & 0 deletions testing-unit-ts-mocks-jest/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2016-2022, Pulumi Corporation. All rights reserved.

import "./resources";

export { audioURL } from "./resources";
9 changes: 9 additions & 0 deletions testing-unit-ts-mocks-jest/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright 2016-2022, Pulumi Corporation. All rights reserved.

import type { Config } from "@jest/types";

const config: Config.InitialOptions = {
preset: "ts-jest",
};

export default config;
17 changes: 17 additions & 0 deletions testing-unit-ts-mocks-jest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "testing-unit-ts-mocks-jest",
"devDependencies": {
"@types/jest": "^28.1.1",
"@types/node": "^14",
"ts-jest": "^28.0.5"
},
"dependencies": {
"@pulumi/aws": "^5.0.0",
"@pulumi/awsx": "^0.40.0",
"@pulumi/pulumi": "^3.0.0",
"node-fetch": "^2.6.7"
},
"scripts": {
"test": "jest"
}
}
59 changes: 59 additions & 0 deletions testing-unit-ts-mocks-jest/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2016-2022, Pulumi Corporation. All rights reserved.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const fetch = require("node-fetch");

// Convert the current time into a speech-friendly string.
function getSpeechText() {
const now = new Date();
now.setSeconds(now.getSeconds() + 6);

const local = new Date(now.toLocaleString("en-US", { timeZone: "America/Los_Angeles" }));
const localHour = local.getHours();
const localMinute = local.getMinutes();
const h = localHour > 12 ? localHour - 12 : localHour;
const m = localMinute < 10 ? `oh ${localMinute}` : localMinute; // So 2:03 -> "two-oh-three"
const s = local.getSeconds();

return `At the tone, the time will be ${h} ${m}. And ${s} seconds.`;
}

export const timeFunction = new aws.lambda.CallbackFunction("time-function", {

// Update the Lambda callback body to convert the current time into an MP3 file.
callback: async () => {
const text = getSpeechText();
const speechURL = `https://translate.google.com/translate_tts?ie=UTF-8&q=${encodeURIComponent(text)}&textLen=${text.length}&tl=en&client=tw-ob`;
const beepURL = "https://www.pulumi.com/uploads/beep.mp3";
const ttsResponse = await fetch(speechURL);
const beepResponse = await fetch(beepURL);
const speech = await ttsResponse.arrayBuffer();
const beep = await beepResponse.arrayBuffer();

// Tack a beep onto the end of the audio returned from Google Translate, then
// render the whole thing as a base-64 encoded string.
const body = Buffer.concat([Buffer.from(speech), Buffer.from(beep)]).toString("base64");

// Return an appropriately shaped HTTP response.
return {
body,
headers: { "Content-Type": "audio/mpeg" },
isBase64Encoded: true,
statusCode: 200,
};
},
});

export const timeURL = new aws.lambda.FunctionUrl("time-url", {
functionName: timeFunction.name,
authorizationType: "NONE",
cors: {
allowOrigins: ["*"],
allowMethods: ["GET"],
},
});

// Export the public URL of our shiny new service.
export const audioURL = timeURL.functionUrl;
18 changes: 18 additions & 0 deletions testing-unit-ts-mocks-jest/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.ts"
]
}

0 comments on commit 69c9a72

Please sign in to comment.