Last active
October 31, 2022 08:28
-
-
Save mikaelvesavuori/9abbb7970aa6720fd5aab37153d2a63b to your computer and use it in GitHub Desktop.
Minimalist integration testing example for Node/JS/TypeScript.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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