testdouble
Version:
A minimal test double library for TDD with JavaScript
114 lines (97 loc) • 3.9 kB
JavaScript
import _ from './wrap/lodash'
import argsMatch from './args-match'
import callsStore from './store/calls'
import log from './log'
import store from './store'
import stringifyArgs from './stringify/arguments'
import stubbingsStore from './store/stubbings'
import notifyAfterSatisfaction from './matchers/notify-after-satisfaction'
import cloneDeepIfPossible from './clone-deep-if-possible'
import symbols from './symbols'
export default (__userDoesRehearsalInvocationHere__, config = {}) => {
const last = callsStore.pop()
ensureRehearsalOccurred(last)
ensureCloneableIfCloneArgs(last, config)
if (callsStore.wasInvoked(last.testDouble, last.args, config)) {
notifyMatchers(last.testDouble, last.args, config)
warnIfStubbed(last.testDouble, last.args)
} else {
log.fail(unsatisfiedErrorMessage(last.testDouble, last.args, config))
}
}
const ensureRehearsalOccurred = (last) => {
if (!last) {
log.error('td.verify', `\
No test double invocation detected for \`verify()\`.
Usage:
verify(myTestDouble('foo'))\
`)
}
}
function ensureCloneableIfCloneArgs (last, config) {
if (config.cloneArgs && cloneDeepIfPossible(last.args) === symbols.uncloneable) {
return log.error('td.verify', `\
Failed to deep-clone arguments. Ensure lodash _.cloneDeep works on them
`)
}
}
const notifyMatchers = (testDouble, expectedArgs, config) => {
_.each(callsStore.where(testDouble, expectedArgs, config), (invocation) => {
notifyAfterSatisfaction(expectedArgs, invocation.args)
})
}
const warnIfStubbed = (testDouble, actualArgs) => {
if (_.some(stubbingsStore.for(testDouble), (stubbing) =>
argsMatch(stubbing.args, actualArgs, stubbing.config))
) {
log.warn('td.verify',
`test double${stringifyName(testDouble)} was both stubbed and verified with arguments (${stringifyArgs(actualArgs)}), which is redundant and probably unnecessary.`,
'https://github.com/testdouble/testdouble.js/blob/main/docs/B-frequently-asked-questions.md#why-shouldnt-i-call-both-tdwhen-and-tdverify-for-a-single-interaction-with-a-test-double'
)
}
}
const unsatisfiedErrorMessage = (testDouble, args, config) =>
baseSummary(testDouble, args, config) +
matchedInvocationSummary(testDouble, args, config) +
invocationSummary(testDouble, args, config)
const stringifyName = (testDouble) => {
const name = store.for(testDouble).name
return name ? ` \`${name}\`` : ''
}
const baseSummary = (testDouble, args, config) =>
`\
Unsatisfied verification on test double${stringifyName(testDouble)}.
Wanted:
- called with \`(${stringifyArgs(args)})\`${timesMessage(config)}${ignoreMessage(config)}.\
`
const invocationSummary = (testDouble, args, config) => {
const calls = callsStore.for(testDouble)
if (calls.length === 0) {
return '\n\n But there were no invocations of the test double.'
} else {
return _.reduce(calls, (desc, call) =>
desc + `\n - called with \`(${stringifyArgs(call.args)})\`.`
, '\n\n All calls of the test double, in order were:')
}
}
const matchedInvocationSummary = (testDouble, args, config) => {
const calls = callsStore.where(testDouble, args, config)
const expectedCalls = config.times || 0
if (calls.length === 0 || calls.length > expectedCalls) {
return ''
} else {
return _.reduce(_.groupBy(calls, 'args'), (desc, callsMatchingArgs, args) =>
desc + `\n - called ${pluralize(callsMatchingArgs.length, 'time')} with \`(${stringifyArgs(callsMatchingArgs[0].args)})\`.`
, `\n\n ${pluralize(calls.length, 'call')} that satisfied this verification:`)
}
}
const pluralize = (x, msg) =>
`${x} ${msg}${x === 1 ? '' : 's'}`
const timesMessage = (config) =>
config.times != null
? ` ${pluralize(config.times, 'time')}`
: ''
const ignoreMessage = (config) =>
config.ignoreExtraArgs != null
? ', ignoring any additional arguments'
: ''