jest-plugin-must-assert
Advanced tools
Comparing version 0.0.2 to 1.0.0
@@ -1,1 +0,5 @@ | ||
require("./src"); | ||
const pathJestAPI = require("./src"); | ||
pathJestAPI({ | ||
logger: console | ||
}); |
{ | ||
"name": "jest-plugin-must-assert", | ||
"version": "0.0.2", | ||
"version": "1.0.0", | ||
"description": "Jest plugin for async tests", | ||
@@ -27,2 +27,3 @@ "main": "index.js", | ||
"jest": "^24.7.1", | ||
"jest-circus": "^24.7.1", | ||
"jest-cli": "^24.7.1", | ||
@@ -29,0 +30,0 @@ "meow": "^5.0.0", |
@@ -6,6 +6,15 @@ # `jest-plugin-must-assert` | ||
## WARNING | ||
## Problem | ||
The plugin is in a working state, but is under construction :construction: | ||
Asynchronous tests could be challenging to get _right_, particulrary for junior | ||
developers or Engineers new to async JavaScript. The most common mistake is an async | ||
test which does not fire any assertions, either due to logic or even syntax errors. | ||
Static analysis(linters) gets close to pointing out the issues, but is not enough to catch logic mistakes. | ||
For this, we require a runtime check that _some_ assertion was ran during the test. | ||
[Jest, unfortunately, has not "failWithoutAssertions" configuration options, so this plugin aims to remedy that.]() | ||
The plugin patches Jest API to force tests without any assertions to fail. In addition | ||
to failing tests without assertions this plugin also patches a bug in Jest which | ||
leads to [assertions "leaking" accross different tests](https://github.com/facebook/jest/issues/8297). | ||
## Install | ||
@@ -17,2 +26,4 @@ | ||
## Use | ||
```js | ||
@@ -31,3 +42,18 @@ ... | ||
mustAssert({ | ||
message: 'Please see https://jestjs.io/docs/en/asynchronous for details on testing async code.' | ||
/** | ||
* Control the task execution during a test. You may log a custom warning message | ||
* from here, throw an error etc. | ||
* | ||
* Default: The default behavior is that mismatched testIds result in ignoring of the task | ||
* and a warning message. | ||
* | ||
* @param {Number} originTestId The unique ID of the test where the task is oginating from | ||
* @param {Number} currentTestId The unique ID of the currently executing test | ||
* @param {Function} log The log method (logger.warn) | ||
* | ||
* @return {Boolean} true/false for whether or not the task should execute | ||
onInvokeTask(originTestId, currentTestId, log) { | ||
return false; | ||
} | ||
logger, | ||
}); | ||
@@ -46,5 +72,6 @@ ``` | ||
## Problem | ||
## Performance | ||
Static analysis is not enough to catch mistakes in asynchronous tests. For this, we require a runtime | ||
check that _some_ assertion was ran during the test. | ||
There is some performance implications of using this plugin as it does add a bit of | ||
overhead, but from testing it's a trivial increase. This plugin has been tested | ||
within a project with 1600+ test suites and over 10k individual tests, with only a negligble slow-down. |
223
src/index.js
@@ -1,128 +0,141 @@ | ||
const consoleMethods = Object.entries(global.console); | ||
const restoreConsole = () => | ||
consoleMethods.forEach(([key, value]) => { | ||
global.console[key] = value; | ||
}); | ||
module.exports = patchJestAPI; | ||
require('zone.js'); | ||
function onInvokeTaskDefault(originZoneId, currentZoneId, log) { | ||
if (originZoneId !== currentZoneId) { | ||
log( | ||
`Test "${current.name}" is attempting to invoke a ${task.type}(${ | ||
task.source | ||
}) after test completion. Ignoring` | ||
); | ||
return false; | ||
} | ||
return true; | ||
} | ||
// NOTE: zone.js patches console methods, avoid that. | ||
restoreConsole(); | ||
function patchJestAPI({ | ||
onInvokeTask = onInvokeTaskDefault, | ||
logger = console, | ||
}) { | ||
const consoleMethods = Object.entries(global.console); | ||
const restoreConsole = () => | ||
consoleMethods.forEach(([key, value]) => { | ||
global.console[key] = value; | ||
}); | ||
// Zone sets itself as a global... | ||
const Zone = global.Zone; | ||
let currentZone = null; | ||
let uniqueIdentifier = 0; | ||
const uuid = () => ++uniqueIdentifier; | ||
require('zone.js'); | ||
const testHasNoExplicitAssertionChecks = () => { | ||
// Some misconfigured test (eg overriding expect itself) | ||
if (!(typeof expect !== 'undefined' && 'getState' in expect)) { | ||
return false; | ||
} | ||
const state = expect.getState(); | ||
return ( | ||
typeof state.expectedAssertionsNumber !== 'number' && | ||
!state.isExpectingAssertions | ||
); | ||
}; | ||
// NOTE: zone.js patches console methods, avoid that. | ||
restoreConsole(); | ||
const exitZone = () => (currentZone = null); | ||
const enterZone = (callback, name, hasDoneCallback) => { | ||
const id = uuid(); | ||
const zone = Zone.root.fork({ | ||
name, | ||
properties: { | ||
id, | ||
}, | ||
onHandleError(delegate, current, target, e) { | ||
// Zone sets itself as a global... | ||
const Zone = global.Zone; | ||
let currentZone = null; | ||
let uniqueIdentifier = 0; | ||
const uuid = () => ++uniqueIdentifier; | ||
const testNeedsAssertionCheck = () => { | ||
// Some misconfigured test (eg overriding expect itself) | ||
if (!(typeof expect !== 'undefined' && 'getState' in expect)) { | ||
return false; | ||
}, | ||
onInvokeTask(delegate, current, target, task, applyThis, applyArgs) { | ||
if (current.get('id') !== currentZone) { | ||
console.warn( | ||
`Test "${current.name}" is attempting to invoke a ${task.type}(${ | ||
task.source | ||
}) after test completion. Ignoring` | ||
); | ||
return; | ||
} | ||
} | ||
const state = expect.getState(); | ||
return ( | ||
typeof state.expectedAssertionsNumber !== 'number' && | ||
!state.isExpectingAssertions | ||
); | ||
}; | ||
return delegate.invokeTask(target, task, applyThis, applyArgs); | ||
}, | ||
}); | ||
const exitZone = () => (currentZone = null); | ||
const enterZone = (callback, name, hasDoneCallback) => { | ||
const id = uuid(); | ||
const zone = Zone.root.fork({ | ||
name, | ||
properties: { | ||
id, | ||
}, | ||
onHandleError(delegate, current, target, e) { | ||
return false; | ||
}, | ||
onInvokeTask(delegate, current, target, task, applyThis, applyArgs) { | ||
if (!onInvokeTask(current.get('id'), currentZone, logger.warn)) { | ||
return; | ||
} | ||
return delegate.invokeTask(target, task, applyThis, applyArgs); | ||
}, | ||
}); | ||
currentZone = id; | ||
currentZone = id; | ||
return zone.wrap(hasDoneCallback ? callback : done => callback(done)); | ||
}; | ||
return zone.wrap(hasDoneCallback ? callback : done => callback(done)); | ||
}; | ||
const wrapTest = (fn, name) => { | ||
let testMustAssert; | ||
const hasDoneCallback = fn.length > 0; | ||
const wrapTest = (fn, name) => { | ||
let testMustAssert; | ||
const hasDoneCallback = fn.length > 0; | ||
if (!hasDoneCallback) { | ||
return () => { | ||
const result = enterZone(fn, name, false)(); | ||
if (!hasDoneCallback) { | ||
return () => { | ||
const result = enterZone(fn, name, false)(); | ||
if (testHasNoExplicitAssertionChecks()) { | ||
if (testNeedsAssertionCheck()) { | ||
expect.hasAssertions(); | ||
} | ||
if ( | ||
typeof result === 'object' && | ||
result != null && | ||
typeof result.then === 'function' | ||
) { | ||
return result.then(exitZone, e => { | ||
exitZone(); | ||
throw e; | ||
}); | ||
} | ||
exitZone(); | ||
return result; | ||
}; | ||
} | ||
return (doneOriginal, ...args) => { | ||
const done = () => { | ||
exitZone(); | ||
doneOriginal(); | ||
}; | ||
const result = enterZone(fn, name, true)(done, ...args); | ||
if (testNeedsAssertionCheck()) { | ||
expect.hasAssertions(); | ||
} | ||
if ( | ||
typeof result === 'object' && | ||
result != null && | ||
typeof result.then === 'function' | ||
) { | ||
return result.then(exitZone, e => { | ||
exitZone(); | ||
throw e; | ||
}); | ||
} | ||
exitZone(); | ||
return result; | ||
}; | ||
} | ||
}; | ||
return (doneOriginal, ...args) => { | ||
const done = () => { | ||
exitZone(); | ||
doneOriginal(); | ||
function enhanceJestImplementationWithAssertionCheck(jestTest) { | ||
return function ehanchedJestMehod(name, fn, timeout) { | ||
return jestTest(name, wrapTest(fn, name), timeout); | ||
}; | ||
const result = enterZone(fn, name, true)(done, ...args); | ||
} | ||
if (testHasNoExplicitAssertionChecks()) { | ||
expect.hasAssertions(); | ||
// Create the enhanced version of the base test() method | ||
const enhancedTest = enhanceJestImplementationWithAssertionCheck(global.test); | ||
Object.keys(global.test).forEach(key => { | ||
if ( | ||
typeof global.test[key] === 'function' && | ||
key !== 'each' && | ||
key !== 'skip' | ||
) { | ||
enhancedTest[key] = enhanceJestImplementationWithAssertionCheck( | ||
global.test[key] | ||
); | ||
} else { | ||
enhancedTest[key] = global.test[key]; | ||
} | ||
}); | ||
return result; | ||
}; | ||
}; | ||
function enhanceJestImplementationWithAssertionCheck(jestTest) { | ||
return function ehanchedJestMehod(name, fn, timeout) { | ||
return jestTest(name, wrapTest(fn, name), timeout); | ||
}; | ||
global.it = enhancedTest; | ||
global.fit = enhancedTest; | ||
global.test = enhancedTest; | ||
} | ||
// Create the enhanced version of the base test() method | ||
const enhancedTest = enhanceJestImplementationWithAssertionCheck(global.test); | ||
Object.keys(global.test).forEach(key => { | ||
if ( | ||
typeof global.test[key] === 'function' && | ||
key !== 'each' && | ||
key !== 'skip' | ||
) { | ||
enhancedTest[key] = enhanceJestImplementationWithAssertionCheck( | ||
global.test[key] | ||
); | ||
} else { | ||
enhancedTest[key] = global.test[key]; | ||
} | ||
}); | ||
global.it = enhancedTest; | ||
global.fit = enhancedTest; | ||
global.test = enhancedTest; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
13673
353
1
74
7