Skip to main content

Unit Testing for Custom Connectors

Overview

It's important to have good unit tests for software - custom components are no exception. You want to catch errors or breaking changes before they wreak havoc on your customers' integrations. Prismatic's Spectral library provides some utility functions to make writing unit tests easier.

In the examples below, we assume that you use the Jest testing framework which is installed by default when you run prism components:init. You can swap Jest out for another testing framework if you like.

Test file naming conventions

To create a unit test file, create a new file alongside your code that has the extension test.ts (rather than .ts). For example, if your code lives in index.ts, create a file named index.test.ts. If you separate out your component actions into actions.ts, create a corresponding actions.test.ts.

Testing component actions

A component action's perform function takes two arguments:

  • context is an object that contains a logger, executionId, instanceState, and stepId.
  • params is an object that contains input parameters as key-value pairs.

Test context parameters are described here. Let's ignore them for now and look at the params object. Consider the example "Format Proper Name" action described previously:

export const properFormatName = action({
display: {
label: "Properly Format Name",
description: "Properly format a person's name (Last, First M.)",
},
perform: async (context, params) => {
if (params.middleName) {
return {
data: `${params.lastName}, ${params.firstName} ${params.middleName[0]}.`,
};
} else {
return { data: `${params.lastName}, ${params.firstName}` };
}
},
inputs: { firstName, middleName, lastName },
});

You can use the ComponentTestHarness class and createHarness helper function to test your actions.

The test harness's action function takes two required and one optional parameters:

  • The action's key (i.e. properFormatName)
  • An object containing input parameters
  • An optional context object containing logger, executionId, instanceState, and stepId

A Jest test file, then, could look like this:

import component from ".";
import { createHarness } from "@prismatic-io/spectral/dist/testing";

const harness = createHarness(component);

describe("Test the Proper Name formatter", () => {
test("Verify first, middle, and last name", async () => {
const result = await harness.action("properFormatName", {
firstName: "John",
middleName: "James",
lastName: "Doe",
});
expect(result.data).toStrictEqual("Doe, John J.");
});
test("Verify first and last name without middle", async () => {
const result = await harness.action("properFormatName", {
firstName: "John",
middleName: null,
lastName: "Doe",
});
expect(result.data).toStrictEqual("Doe, John");
});
});

You can then run yarn run jest, and Jest will run each test, returning an error code if a test failed.

Verifying correct logging in action tests

You may want to verify that your action generates some logs of particular severities in certain situations. In addition to step results, the test utility's invoke function returns an object, loggerMock, with information on what was logged during the action invocation.

You can use Jest to verify that certain lines were logged like this:

import { myExampleAction } from "./actions";
import { invoke } from "@prismatic-io/spectral/dist/testing";

test("Ensure that an error is logged", async () => {
const level = "error";
const message = "Error code 42 occurred.";
const { loggerMock } = await invoke(myExampleAction, {
exampleInput1: "exampleValue1",
exampleInput2: "exampleValue2",
});
expect(loggerMock[level]).toHaveBeenCalledWith(message);
});

In the above example, the test would pass if an error log line of Error code 42 occurred. were generated, and would fail otherwise.

Providing test connection inputs to an action test

Many actions require a connection to interact with third-party services. You can create a connection object the createConnection function from @prismatic-io/spectral/dist/testing:

import {
createConnection,
createHarness,
} from "@prismatic-io/spectral/dist/testing";
import component from ".";
import { myConnection } from "./connections";

const harness = createHarness(component);

const myBasicAuthTestConnection = createConnection(myConnection, {
username: "myUsername",
password: "myPassword",
});

describe("test my action", () => {
test("verify the return value of my action", async () => {
const result = await harness.action("myAction", {
someInput: "abc-123",
connection: myBasicAuthTestConnection,
someOtherInput: "def-456",
});
});
});

It's not good practice to hard-code authorization secrets. Please use best practices, like setting environment variables to store secrets in your CI/CD environment:

import { createConnection } from "@prismatic-io/spectral/dist/testing";
import { myConnection } from "./connections";

const myBasicAuthTestConnection = createConnection(myConnection, {
username: process.env.ACME_ERP_USERNAME,
password: process.env.ACME_ERP_PASSWORD,
});
Use an Existing Integration's Connections for Testing

If you would like to fetch an access key from an existing OAuth 2.0 connection in an integration (or username / password, API key, etc.), leverage the prism components:dev:run command to fetch the connection's fields and tokens. You can then reference the PRISMATIC_CONNECTION_VALUE environment variable in your Jest tests.

More info is in our prism docs.

Testing a trigger

Testing a trigger is similar to testing an action, except you use the harness.trigger function instead. For example, if you want to use Jest to test the csvTrigger outlined above, your test could look like this:

import component from ".";
import {
createHarness,
defaultTriggerPayload,
} from "@prismatic-io/spectral/dist/testing";

const harness = createHarness(component);

describe("test csv webhook trigger", () => {
test("verify the return value of the csv webhook trigger", async () => {
const payload = defaultTriggerPayload(); // The payload you can expect a generic trigger to receive

payload.rawBody.data = "first,last,age\nJohn,Doe,30\nJane,Doe,31";
payload.headers.contentType = "text/csv";
payload.headers["x-confirmation-code"] = "some-confirmation-code-123";

const expectedData = [
{ first: "John", last: "Doe", age: "30" },
{ first: "Jane", last: "Doe", age: "31" },
];

const expectedResponse = {
statusCode: 200,
contentType: "text/plain; charset=utf-8",
body: payload.headers["x-confirmation-code"],
};

const {
payload: {
body: { data },
},
response,
} = await harness.trigger("csvTrigger", null, payload, {
hasHeader: true,
});

expect(data).toStrictEqual(expectedData);
expect(response).toStrictEqual(expectedResponse);
});
});

Testing components from the CLI

The prism CLI tool provides two commands for testing custom components:

  • prism components:dev:run fetches an integration's active connection and saves the fields as an environment variable so you can run unit tests and other commands locally. This is helpful, since many unit tests require an access token from a validated OAuth 2.0 connection - this provides a way of fetching the token from a connection you've already authenticated in the Prismatic integration designer.
  • prism components:dev:test publishes your component under a temporary name, and runs a single-action test integration for you that tests the action. This is helpful for quickly testing an action in the real Prismatic integration runner environment.

Access connections for local testing

The prism components:dev:run command fetches an active connection from the Prismatic integration designer, so you can use the connection's fields for unit testing. The connection's values are set to an environment variable named PRISMATIC_CONNECTION_VALUE, which can be used by a subsequent command.

In this example, we use printenv to print the environment variable, and pipe the result into jq for pretty printing:

prism components:dev:run \
--integrationId SW50ZEXAMPLE \
--connectionKey "Dropbox Connection" -- printenv PRISMATIC_CONNECTION_VALUE | jq

{
"token": {
"access_token": "sl.EXAMPLE",
"token_type": "bearer",
"expires_in": 14400,
"expires_at": "2022-10-13T20:09:53.739Z",
"refresh_token": "EXAMPLE"
},
"context": {
"code": "sU4pEXAMPLE",
"state": "SW5zdEXAMPLE"
},
"fields": {
"clientId": "EXAMPLE",
"clientSecret": "EXAMPLE"
}
}

Note that the command you want to run with the environment variable should follow a --.

Within your unit test code, you can use harness.connectionValue(), which pulls in the PRISMATIC_CONNECTION_VALUE environment variable into a connection that you can use for tests:

Use PRISMATIC_CONNECTION_VALUE for a Jest test
import { createHarness } from "@prismatic-io/spectral/dist/testing";
import { oauthConnection } from "./connections";
import component from ".";

// Initialize a testing harness
const harness = createHarness(component);

// Parse the OAuth 2.0 connection from the PRISMATIC_CONNECTION_VALUE environment variable
const parsedConnection = harness.connectionValue(oauthConnection);

describe("listFolder", () => {
test("listRootFolder", async () => {
const result = await harness.action("listFolder", {
dropboxConnection: parsedConnection, // Pass in our connection
path: "/",
});
const files = result["data"]["result"]["entries"];
// Verify a folder named "Public" exists in the response
expect(files).toEqual(
expect.arrayContaining([expect.objectContaining({ name: "Public" })]),
);
});
});

From your component, you can then run:

prism components:dev:run \
--integrationId SW50ZEXAMPLE \
--connectionKey "Dropbox Connection" -- npm run jest

Run a test of an action from the command line

The prism components:dev:test command allows you to test an action quickly from the command line in the real integration runner environment.

  • Run prism components:dev:test from your component's root directory.
  • You will be prompted to select an action to test. Select one.
  • For each input of the action, supply a value
  • If your action requires a connection, you will be prompted for values for that connection (username, password, client_id, etc).
    • If your action requires an OAuth 2.0 connection, a web browser will open to handle the OAuth flow.

Once all inputs are entered, your action will run in the integration runner, and you will see logs from your action.

Test run environment files

You do not need to enter the same inputs each time you want to run a test of your action. To set some values for your test inputs, create a new file called .env in the same directory where you're invoking prism and enter your inputs and values as key/value pairs. For example, if you plan to leave cursor and limit inputs blank, set path to /, and you have an OAuth client ID and secret that you want to use each time, your .env file can look like this:

CURSOR=
LIMIT=
PATH=/
CLIENT_ID=xlexample
CLIENT_SECRET=4yexample