UNPKG

node-tdd

Version:

Drop in extension for mocha to abstract commonly used test setups

278 lines (265 loc) 9.11 kB
import assert from 'assert'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'smart-fs'; import callsites from 'callsites'; import get from 'lodash.get'; import minimist from 'minimist'; import tmp from 'tmp'; import Joi from 'joi-strict'; import RequestRecorder from '../modules/request-recorder.js'; import EnvManager from '../modules/env-manager.js'; import TimeKeeper from '../modules/time-keeper.js'; import LogRecorder from '../modules/log-recorder.js'; import RandomSeeder from '../modules/random-seeder.js'; import CacheClearer from '../modules/cache-clearer.js'; import { getParents, genCassetteName } from './mocha-test.js'; const mocha = { it, specify, describe, context, before, after, beforeEach, afterEach }; const desc = (suiteName, optsOrTests, testsOrNull = null) => { const opts = testsOrNull === null ? {} : optsOrTests; const tests = testsOrNull === null ? optsOrTests : testsOrNull; const filenameOrUrl = callsites()[1].getFileName(); const testFile = path.resolve( filenameOrUrl.startsWith('file://') ? fileURLToPath(filenameOrUrl) // eslint-disable-next-line @blackflux/rules/c8-prevent-ignore /* c8 ignore next */ : filenameOrUrl ); const resolve = (name) => path.join( path.dirname(testFile), name.replace(/\$FILENAME/g, path.basename(testFile)) ); Joi.assert(opts, Joi.object().keys({ useTmpDir: Joi.boolean().optional(), useNock: Joi.boolean().optional(), nockFolder: Joi.string().optional(), nockModifiers: Joi.object().optional().pattern(Joi.string(), Joi.function()), nockStripHeaders: Joi.boolean().optional(), nockReqHeaderOverwrite: Joi.object().optional() .pattern(Joi.string(), Joi.alternatives(Joi.function(), Joi.string())), fixtureFolder: Joi.string().optional(), envVarsFile: Joi.string().optional(), envVars: Joi.object().optional().unknown(true).pattern(Joi.string(), Joi.string()), clearCache: Joi.boolean().optional(), timestamp: Joi.alternatives( Joi.number().integer().min(0), Joi.date().iso() ).optional(), record: Joi.any().optional(), cryptoSeed: Joi.string().optional(), cryptoSeedReseed: Joi.when('cryptoSeed', { is: Joi.string(), then: Joi.boolean().optional(), otherwise: Joi.forbidden() }), timeout: Joi.number().optional().min(0) }), 'Bad Options Provided'); const useTmpDir = get(opts, 'useTmpDir', false); const useNock = get(opts, 'useNock', false); const nockFolder = resolve(get(opts, 'nockFolder', '$FILENAME__cassettes')); const nockModifiers = get(opts, 'nockModifiers', {}); const nockStripHeaders = get(opts, 'nockStripHeaders', false); const nockReqHeaderOverwrite = get(opts, 'nockReqHeaderOverwrite', {}); const fixtureFolder = resolve(get(opts, 'fixtureFolder', '$FILENAME__fixtures')); const envVarsFile = resolve(get(opts, 'envVarsFile', '$FILENAME.env.yml')); const envVars = get(opts, 'envVars', null); const clearCache = get(opts, 'clearCache', true); const timestamp = get(opts, 'timestamp', null); const record = get(opts, 'record', false); const cryptoSeed = get(opts, 'cryptoSeed', null); const cryptoSeedReseed = get(opts, 'cryptoSeedReseed', false); const timeout = get(opts, 'timeout', null); const nockHeal = get(minimist(process.argv.slice(2)), 'nock-heal', false); let dir = null; let requestRecorder = null; let cacheClearer = null; let envManagerFile = null; let envManagerDesc = null; let timeKeeper = null; let logRecorder = null; let randomSeeder = null; const getArgs = () => ({ capture: async (fn) => { try { await fn(); } catch (e) { return e; } throw new assert.AssertionError({ message: 'expected [Function] to throw an error' }); }, fixture: (name) => { const filepath = fs.guessFile(path.join(fixtureFolder, name)); if (filepath === null) { throw new assert.AssertionError({ message: `fixture "${name}" not found or ambiguous` }); } return fs.smartRead(filepath); }, ...(dir === null ? {} : { dir }), ...(logRecorder === null ? {} : { recorder: { verbose: logRecorder.verbose, get: logRecorder.get, reset: logRecorder.reset } }) }); let beforeCb = () => {}; let afterCb = () => {}; let beforeEachCb = () => {}; let afterEachCb = () => {}; // eslint-disable-next-line func-names mocha.describe(suiteName, function () { return (async () => { if (timeout !== null) { this.timeout(timeout); } // eslint-disable-next-line func-names mocha.before(function () { return (async () => { if (clearCache !== false) { cacheClearer = CacheClearer(); } if (getParents(this.test).length === 3 && fs.existsSync(envVarsFile)) { envManagerFile = EnvManager({ envVars: fs.smartRead(envVarsFile), allowOverwrite: false }); envManagerFile.apply(); } if (envVars !== null) { envManagerDesc = EnvManager({ envVars, allowOverwrite: false }); envManagerDesc.apply(); } if (timestamp !== null) { timeKeeper = TimeKeeper({ timestamp }); timeKeeper.inject(); } if (cryptoSeed !== null) { randomSeeder = RandomSeeder({ seed: cryptoSeed, reseed: cryptoSeedReseed }); randomSeeder.inject(); } if (useNock === true) { requestRecorder = RequestRecorder({ cassetteFolder: `${nockFolder}/`, stripHeaders: nockStripHeaders, reqHeaderOverwrite: nockReqHeaderOverwrite, strict: true, heal: nockHeal, modifiers: nockModifiers }); } await beforeCb.call(this); })(); }); // eslint-disable-next-line func-names mocha.after(function () { return (async () => { if (requestRecorder !== null) { requestRecorder.shutdown(); requestRecorder = null; } if (randomSeeder !== null) { randomSeeder.release(); randomSeeder = null; } if (timeKeeper !== null) { timeKeeper.release(); timeKeeper = null; } if (envManagerDesc !== null) { envManagerDesc.unapply(); envManagerDesc = null; } if (envManagerFile !== null) { envManagerFile.unapply(); envManagerFile = null; } await afterCb.call(this); })(); }); // eslint-disable-next-line func-names mocha.beforeEach(function () { return (async () => { if (cacheClearer !== null) { cacheClearer.inject(); } if (useTmpDir === true) { tmp.setGracefulCleanup(); dir = tmp.dirSync({ keep: false, unsafeCleanup: true }).name; } if (useNock === true) { await requestRecorder.inject(genCassetteName(this.currentTest)); } if (record !== false) { logRecorder = LogRecorder({ verbose: process.argv.slice(2).includes('--verbose'), logger: record }); logRecorder.inject(); } await beforeEachCb.call(this, getArgs()); })(); }); // eslint-disable-next-line func-names mocha.afterEach(function () { return (async () => { await afterEachCb.call(this, getArgs()); if (logRecorder !== null) { logRecorder.release(); logRecorder = null; } if (requestRecorder !== null) { await requestRecorder.release(); } if (dir !== null) { dir = null; } if (cacheClearer !== null) { cacheClearer.release(); } })(); }); const globalsPrev = Object.keys(mocha) .reduce((p, key) => Object.assign(p, { [key]: global[key] })); global.it = (testName, fn) => mocha.it( testName, fn.length === 0 || /^[^(=]*\({/.test(fn.toString()) // eslint-disable-next-line func-names ? function () { return fn.call(this, getArgs()); } // eslint-disable-next-line func-names : function (done) { return fn.call(this, done); } ); global.specify = global.it; global.describe = desc; global.context = global.describe; global.before = (fn) => { beforeCb = fn; }; global.after = (fn) => { afterCb = fn; }; global.beforeEach = (fn) => { beforeEachCb = fn; }; global.afterEach = (fn) => { afterEachCb = fn; }; await tests.call(this); Object.entries(globalsPrev).forEach(([k, v]) => { global[k] = v; }); })(); }); }; export default desc;