testdouble
Version:
A minimal test double library for TDD with JavaScript
127 lines (112 loc) • 3.71 kB
JavaScript
import _ from './wrap/lodash'
import proxySafeCloneDeepWith from './wrap/proxy-safe-clone-deep-with'
import callsStore from './store/calls'
import store from './store'
import stringifyArgs from './stringify/arguments'
import stubbingsStore from './store/stubbings'
import symbols from './symbols'
export default function explain (testDouble) {
if (_.isFunction(testDouble)) {
return explainFunction(testDouble)
} else if (_.isObject(testDouble)) {
return explainObject(testDouble)
} else {
return explainNonTestDouble(testDouble)
}
}
function explainObject (obj) {
const { explanations, children } = explainChildren(obj)
return {
name: null,
callCount: 0,
calls: [],
description: describeObject(explanations),
children,
isTestDouble: explanations.length > 0
}
}
function explainChildren (thing) {
const explanations = []
const children = proxySafeCloneDeepWith(thing, (val, key, obj, stack) => {
if (_.isFunction(val) && stack) {
return _.tap(explainFunction(val), (explanation) => {
if (explanation.isTestDouble) explanations.push(explanation)
})
}
})
return { explanations, children }
}
function describeObject (explanations) {
const count = explanations.length
if (count === 0) return 'This object contains no test doubles'
return `This object contains ${count} test double function${count > 1 ? 's' : ''}: [${_.map(explanations, e =>
`"${e.name}"`
).join(', ')}]`
}
function explainFunction (testDouble) {
if (store.for(testDouble, false) == null) { return explainNonTestDouble(testDouble) }
const calls = callsStore.for(testDouble)
const stubs = stubbingsStore.for(testDouble)
const { children } = explainChildren(testDouble)
return {
name: store.for(testDouble).name,
callCount: calls.length,
calls,
description:
testdoubleDescription(testDouble, stubs, calls) +
stubbingDescription(stubs) +
callDescription(calls),
children,
isTestDouble: true
}
}
function explainNonTestDouble (thing) {
return ({
name: undefined,
callCount: 0,
calls: [],
description: `This is not a test double${_.isFunction(thing) ? ' function' : ''}.`,
isTestDouble: false
})
}
function testdoubleDescription (testDouble, stubs, calls) {
return `This test double ${stringifyName(testDouble)}has ${stubs.length} stubbings and ${calls.length} invocations.`
}
function stubbingDescription (stubs) {
return stubs.length > 0
? _.reduce(stubs, (desc, stub) =>
desc + `\n - when called with \`(${stringifyArgs(stub.args)})\`, then ${planFor(stub)} ${argsFor(stub)}.`
, '\n\nStubbings:')
: ''
}
function planFor (stub) {
switch (stub.config.plan) {
case 'thenCallback': return 'callback'
case 'thenResolve': return 'resolve'
case 'thenReject': return 'reject'
default: return 'return'
}
}
function argsFor (stub) {
switch (stub.config.plan) {
case 'thenCallback': return `\`(${stringifyArgs(stub.stubbedValues, ', ')})\``
default: return stringifyArgs(stub.stubbedValues, ', then ', '`')
}
}
function callDescription (calls) {
return calls.length > 0
? _.reduce(calls, (desc, call) => {
let argDescription
if (call.cloneArgs !== symbols.uncloneable) {
argDescription = `\`(${stringifyArgs(call.cloneArgs)})\`.`
} else {
argDescription = `\`(${stringifyArgs(call.args)})\` [Cloning argument values failed; displaying current references]`
}
return desc + `\n - called with ${argDescription}`
}, '\n\nInvocations:')
: ''
}
function stringifyName (testDouble) {
const name = store.for(testDouble).name
return name ? `\`${name}\` ` : ''
}