Skip to content

Instantly share code, notes, and snippets.

@mikaelvesavuori
Last active October 31, 2022 08:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mikaelvesavuori/9abbb7970aa6720fd5aab37153d2a63b to your computer and use it in GitHub Desktop.
Save mikaelvesavuori/9abbb7970aa6720fd5aab37153d2a63b to your computer and use it in GitHub Desktop.
Minimalist integration testing example for Node/JS/TypeScript.
export const assertions = [
{
name: 'It should DO SOMETHING',
payload: {
method: 'POST',
path: 'DoSomething',
headers: {
'X-Client-Version': 1
},
body: {
userName: 'Sam Person',
actions: [
{
Id: '2n022yd',
ActionType: 'CONFIRMED'
}
]
}
},
schema: {
type: 'object',
properties: {
systemId: { type: 'string' }
},
required: ['systemId'],
additionalProperties: false
}
},
{
name: 'It should UPDATE SOMETHING',
payload: {
method: 'PATCH',
path: 'UpdateSomething',
body: {
id: 'abc123',
newValue: 'qwerty',
}
},
expected: 'OK'
},
{
name: 'It should GET SOMETHING',
payload: {
method: 'GET',
path: 'GetSomething',
urlParams: {
systemId: 'something',
user: 'something'
}
},
schema: {
type: 'object',
properties: {
id: { type: 'string' },
version: { type: 'number' },
hasDoneSomething: { type: 'boolean' }
},
required: ['id', 'version', 'hasDoneSomething'],
additionalProperties: false
}
}
];
// import fetch from 'node-fetch'; ---> Only needed when using less than Node 18
import Ajv from 'ajv';
import { assertions } from './assertions';
const INTEGRATION_ENDPOINT = 'https://my-web-api.acmecorp.xyz';
const AUTH_TOKEN = 'something-here';
async function runIntegrationTests() {
if (!INTEGRATION_ENDPOINT) throw new Error('Missing INTEGRATION_ENDPOINT!');
let testsFailed = false;
const tests = assertions.map(async (assertion: any) => {
return new Promise(async (resolve, reject) => {
const { name, payload, schema, expected } = assertion;
const { method, path, headers, body, urlParams } = payload;
// Use auth header if needed
headers.Authorization = AUTH_TOKEN;
console.log(`Running integration test: "${name}"`);
const response = await fetchData(
`${INTEGRATION_ENDPOINT}/${path}`,
headers,
method,
body,
urlParams
);
if (!response) throw new Error('❌ No response!');
/**
* If there is an Ajv matching schema use that to check,
* else use an exact comparison to check.
*/
const isMatch = schema
? test(schema, response)
: JSON.stringify(response) === JSON.stringify(expected);
if (isMatch) resolve(true);
else {
testsFailed = true;
reject({ name, response });
}
});
});
Promise.all(tests)
.catch((error) => error)
.then((result) => {
if (testsFailed) {
console.log(
`❌ Failed integration test: "${result.name}" --> ${JSON.stringify(result.response)}`
);
process.exit(1);
} else {
console.log('✅ Passed all integration tests');
}
});
}
/**
* @description Wrapper for fetching data.
*/
async function fetchData(
url: string,
headers: Record<string, any>,
method: 'POST' | 'PATCH' | 'GET',
body: any,
urlParams: Record<string, any>
): Promise<any> {
/**
* If we have `urlParams` (which we can infer meaning that it's a GET case), then
* manually spread these (known) properties first into a full URL.
*
* Else just use it as-is.
*/
const fetchUrl = urlParams
? `${url}${getParamsString(urlParams)}`
: url;
const response = await fetch(fetchUrl, {
headers,
body: body ? JSON.stringify(body) : undefined,
method
});
// If this is OK and status 204 ("No content") then we can safely return
if (response.ok && response.status === 204) return 'OK';
const text = await response.text();
// Return text or JSON depending on what it actually was
try {
const data = JSON.parse(text);
return data;
} catch (error) {
return text;
}
}
const escapeString = (value: any) => {
if (typeof value === 'string') return value;
return JSON.stringify(value).replace(/\s/g, '%20').replace(/"/gi, '\\"');
};
const getParamsString = (urlParams: Record<string, any>) =>
Object.entries(urlParams).reduce(
(previousValue: [string, any], currentValue: any[], index: number): any => {
let paramValue = index === 1 ? `?` : `${previousValue}&`;
// On the first run this will include the "zeroth" value
if (index === 1) {
const [key, value] = previousValue;
paramValue += `${key}=${escapeString(value)}&`;
}
const [key, value] = currentValue;
paramValue += `${key}=${escapeString(escapeString(value))}`;
return paramValue;
}
);
/**
* @description Run a test by validating a schema with Ajv.
*/
function test(schema: any, data: any): boolean {
const isArray = Array.isArray(data);
if (isArray) data = data[0]; // Use the first item in an array if this is one
const ajv = new Ajv();
const validate = ajv.compile(schema);
const isValid = validate(data);
return isValid;
}
runIntegrationTests();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment