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 alogger
,executionId
,instanceState
, andstepId
.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 containinglogger
,executionId
,instanceState
, andstepId
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,
});
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:
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