testdouble
Version:
A minimal test double library for TDD with JavaScript
117 lines (102 loc) • 3.44 kB
JavaScript
import _ from '../wrap/lodash'
import argsMatch from '../args-match'
import isCallback from '../matchers/is-callback'
import notifyAfterSatisfaction from '../matchers/notify-after-satisfaction'
import config from '../config'
import log from '../log'
import store from './index'
export default {
add (testDouble, args, stubbedValues, config) {
return store.for(testDouble).stubbings.push({
callCount: 0,
stubbedValues,
args: config.cloneArgs ? _.cloneDeep(args) : args,
config
})
},
invoke (testDouble, actualArgs, actualContext) {
const stubbing = stubbingFor(testDouble, actualArgs)
if (stubbing) {
notifyAfterSatisfaction(stubbing.args, actualArgs)
return executePlan(stubbing, actualArgs, actualContext)
}
},
for (testDouble) {
return store.for(testDouble).stubbings
}
}
const stubbingFor = (testDouble, actualArgs) =>
_.findLast(store.for(testDouble).stubbings, stubbing =>
isSatisfied(stubbing, actualArgs))
const executePlan = (stubbing, actualArgs, actualContext) => {
const value = stubbedValueFor(stubbing)
stubbing.callCount += 1
invokeCallbackFor(stubbing, actualArgs)
switch (stubbing.config.plan) {
case 'thenReturn': return value
case 'thenDo': return value.apply(actualContext, actualArgs)
case 'thenThrow': throw value
case 'thenResolve': return createPromise(stubbing, value, true)
case 'thenReject': return createPromise(stubbing, value, false)
}
}
const invokeCallbackFor = (stubbing, actualArgs) => {
if (_.some(stubbing.args, isCallback)) {
_.each(stubbing.args, (expectedArg, i) => {
if (isCallback(expectedArg)) {
callCallback(stubbing, actualArgs[i], callbackArgs(stubbing, expectedArg))
}
})
}
}
const callbackArgs = (stubbing, expectedArg) => {
if (expectedArg.args != null) {
return expectedArg.args
} else if (stubbing.config.plan === 'thenCallback') {
return stubbing.stubbedValues
} else {
return []
}
}
const callCallback = (stubbing, callback, args) => {
if (stubbing.config.delay) {
_.delay(callback, stubbing.config.delay, ...args)
} else if (stubbing.config.defer) {
_.defer(callback, ...args)
} else {
callback(...args) // eslint-disable-line
}
}
const createPromise = (stubbing, value, willResolve) => {
const Promise = config().promiseConstructor
ensurePromise(Promise)
return new Promise((resolve, reject) => {
callCallback(stubbing, () =>
willResolve ? resolve(value) : reject(value)
, [value])
})
}
const stubbedValueFor = (stubbing) =>
stubbing.callCount < stubbing.stubbedValues.length
? stubbing.stubbedValues[stubbing.callCount]
: _.last(stubbing.stubbedValues)
const isSatisfied = (stubbing, actualArgs) =>
argsMatch(stubbing.args, actualArgs, stubbing.config) &&
hasTimesRemaining(stubbing)
const hasTimesRemaining = (stubbing) =>
stubbing.config.times == null
? true
: stubbing.callCount < stubbing.config.times
const ensurePromise = (Promise) => {
if (Promise == null) {
return log.error('td.when', `\
no promise constructor is set (perhaps this runtime lacks a native Promise
function?), which means this stubbing can't return a promise to your
subject under test, resulting in this error. To resolve the issue, set
a promise constructor with \`td.config\`, like this:
td.config({
promiseConstructor: require('bluebird')
})\
`)
}
}