Skip to content

Instantly share code, notes, and snippets.

@ssube
Last active February 11, 2018 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ssube/566cdf57af64f9f52e5efb456b891c89 to your computer and use it in GitHub Desktop.
Save ssube/566cdf57af64f9f52e5efb456b891c89 to your computer and use it in GitHub Desktop.
async test harness
import {AsyncHook, createHook} from 'async_hooks';
import {expect} from 'chai';
// this will pull Mocha internals out of the stacks
const {stackTraceFilter} = require('mocha/lib/utils');
const filterStack = stackTraceFilter();
type AsyncMochaTest = (this: Mocha.ITestCallbackContext | void, done: MochaDone) => Promise<void>;
type AsyncMochaSuite = (this: Mocha.ISuiteCallbackContext) => Promise<void>;
const UNHANDLED_REJECTION = 'unhandledRejection';
export function describeAsync(description: string, cb: AsyncMochaSuite): Mocha.ISuite {
return describe(description, function track() {
const tracker = new Tracker();
beforeEach(() => {
tracker.enable();
});
afterEach(() => {
const leaked = tracker.size;
tracker.dump();
tracker.clear();
if (leaked > 0) {
throw new Error('test leaked async resources');
}
});
const suite: PromiseLike<void> | undefined = cb.call(this);
if (!suite || !suite.then) {
console.error(`test suite '${description}' did not return a promise`);
}
return suite;
});
}
/**
* Run an asynchronous test with unhandled rejection guards.
*
* This function may not have any direct test coverage. It is too simple to reasonably mock.
*/
export function itAsync(expectation: string, cb: AsyncMochaTest): Mocha.ITest {
return it(expectation, function track(done) {
try {
const test: PromiseLike<void> | undefined = cb.call(this);
if (!test || !test.then) {
console.error(`test '${expectation}' did not return a promise`);
} else {
test.then((value: any) => {
done();
}, (err: Error) => {
done(err);
});
}
} catch (err) {
console.error('test leaked synchronous error', err);
done(err);
}
});
}
export function delay(ms: number) {
return new Promise((res) => setTimeout(() => res(), ms));
}
export interface TrackedResource {
source: string;
triggerAsyncId: number;
type: string;
};
/**
* Async resource tracker using node's internal hooks.
*
* This probably won't work in a browser. It does not hold references to the resource, to avoid leaks.
* Adapted from https://gist.github.com/boneskull/7fe75b63d613fa940db7ec990a5f5843#file-async-dump-js
*/
export class Tracker {
private hook: AsyncHook;
private resources: Map<number, TrackedResource>;
constructor() {
this.resources = new Map();
this.hook = createHook({
destroy: (id: number) => {
this.resources.delete(id);
},
init: (id: number, type: string, triggerAsyncId: number) => {
const source = filterStack((new Error()).stack || 'unknown');
this.resources.set(id, {
source,
triggerAsyncId,
type
});
}
});
}
public clear() {
this.resources.clear();
}
public dump() {
this.hook.disable();
console.error(`listing ${this.resources.size} tracked async resources`);
this.resources.forEach((res, id) => {
console.error(`id: ${id}`);
console.error(`type: ${res.type}`);
console.error(res.source);
console.error('\n');
});
}
public enable() {
this.hook.enable();
console.error('enabling async resource tracker');
}
public get size(): number {
return this.resources.size;
}
}
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import {ineeda} from 'ineeda';
import * as sinonChai from 'sinon-chai';
import * as sourceMapSupport from 'source-map-support';
/**
* This will break the whole test run if any test leaks an unhandled rejection.
*
* To ensure only a single test breaks, make sure to wrap each test with the `handleRejection` helper.
*/
process.on('unhandledRejection', (reason, promise) => {
console.error('unhandled error during tests', reason);
process.exit(1);
});
chai.use(chaiAsPromised);
chai.use(sinonChai);
ineeda.intercept({
then: null,
unsubscribe: null
});
sourceMapSupport.install();
const context = (require as any).context('.', true, /Test.*$/);
context.keys().forEach(context);
export default context;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment