testdouble
Advanced tools
Comparing version 2.0.0-pre.4 to 2.0.0-pre.5
# Change Log | ||
## [v2.0.0-pre.5](https://github.com/testdouble/testdouble.js/tree/v2.0.0-pre.5) (2017-03-13) | ||
[Full Changelog](https://github.com/testdouble/testdouble.js/compare/v2.0.0-pre.4...v2.0.0-pre.5) | ||
**Closed issues:** | ||
- contains not working against IIFE objects [\#192](https://github.com/testdouble/testdouble.js/issues/192) | ||
- Allow matchers inside objects [\#160](https://github.com/testdouble/testdouble.js/issues/160) | ||
- Support invoking callbacks with arbitrary timing [\#106](https://github.com/testdouble/testdouble.js/issues/106) | ||
**Merged pull requests:** | ||
- Async callbacks [\#205](https://github.com/testdouble/testdouble.js/pull/205) ([searls](https://github.com/searls)) | ||
## [v2.0.0-pre.4](https://github.com/testdouble/testdouble.js/tree/v2.0.0-pre.4) (2017-03-12) | ||
@@ -4,0 +17,0 @@ [Full Changelog](https://github.com/testdouble/testdouble.js/compare/v2.0.0-pre.3...v2.0.0-pre.4) |
@@ -541,2 +541,79 @@ # Stubbing behavior | ||
#### defer | ||
[Note: you probably don't need this option. Using it everywhere smells of overly | ||
defensive specifications.] | ||
By default, callback stubbings (whether configured via the `td.callback` matcher | ||
or by invoking `thenCallback`) are invoked synchronously. Since testdouble.js is | ||
designed for isolated unit tests, this is usually what you want, because | ||
comprehending and troubleshooting synchronous test scripts will always be much | ||
simpler than asynchronous ones. However, in the event that you want to ensure | ||
the subject isn't inadvertently relying on this synchronous execution of | ||
callbacks, you can ensure those callbacks are scheduled to a later execution | ||
stack by setting the `defer` option to `true`. | ||
Take this example of an erroneously passing test: | ||
```js | ||
// Subject under test | ||
function printBalance (id, fetchBalance, print) { | ||
var balance; | ||
fetchBalance(id, function (er, amount) { | ||
balance = amount | ||
}) | ||
print('Your balance is ' + balance) | ||
} | ||
// Test body | ||
var fetchBalance = td.function('.fetchBalance') | ||
var print = td.function('.print') | ||
td.when(fetchBalance(42)).thenCallback(null, 1337) | ||
printBalance(42, fetchBalance, print) | ||
td.verify(print('Your balance is 1337')) | ||
``` | ||
The above passes, but suppose that in practice `fetchBalance` is going to | ||
invoke the callback asynchronously—if that's the case, then this passing test | ||
will be lying to us! To guard against this category of test smells, you can set | ||
the `defer` option when stubbing the async dependency, like so: | ||
``` | ||
td.when(fetchBalance(42), {defer: true}).thenCallback(null, 1337) | ||
``` | ||
Now the test above will fail—shiny! Keep in mind that you'll have to make the | ||
overall test asynchronous (e.g. a `done` callback in Mocha/Jasmine) when using | ||
the `defer` option. | ||
[Note: while this option is also supported for the Promise stubbings | ||
`thenResolve` and `thenReject`, all standard Promise implementations will | ||
already ensure your event handlers will fire asynchronously on a later call | ||
stack.] | ||
#### delay | ||
[Note: you _really_ probably don't need this. You might need `defer` above, but | ||
only reach for this `delay` option when your subject's behavior depends on the | ||
order in which various async operations are completed.] | ||
When using `td.callback`, `thenCallback`, `thenResolve`, or `thenReject`, you | ||
can use the `delay` option to wait a set number of milliseconds before the | ||
operation completes. | ||
Here's a quick and silly example of what this option entails: | ||
```js | ||
var fetch td.function('.fetch') | ||
td.when(fetch('/A'), {delay: 20}).thenCallback(null, 1) | ||
td.when(fetch('/B'), {delay: 10}).thenCallback(null, 2) | ||
td.when(fetch('/C'), {delay: 5}).thenResolve(3) | ||
fetch('/A', function (er, result) {}) // will be invoked 3rd | ||
fetch('/B', function (er, result) {}) // will be invoked 2nd | ||
fetch('/C').then(function (result) {}) // will be invoked 1st | ||
``` | ||
## Congratulations! | ||
@@ -543,0 +620,0 @@ |
@@ -7,122 +7,206 @@ // Generated by CoffeeScript 1.12.4 | ||
}); | ||
describe('when', function() { | ||
When(function() { | ||
return this.returnValue = this.testDouble('/foo', (function(_this) { | ||
return function(er, results) { | ||
_this.callbackInvoked = true; | ||
_this.er = er; | ||
return _this.results = results; | ||
}; | ||
})(this)); | ||
}); | ||
context('VERBOSE: using td.callback() as a matcher with a thenReturn chain', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback(null, 'some results'))).thenReturn('pandas'); | ||
}); | ||
Then(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === 'pandas'; | ||
}); | ||
}); | ||
context('TERSE: use thenCallback chain with td.callback implied as last arg', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo')).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.callbackInvoked = true; | ||
}); | ||
And(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === void 0; | ||
}); | ||
}); | ||
context('ORDER-EXPLICIT: use td.callback as a marker with a thenCallback chain', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback)).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === void 0; | ||
}); | ||
}); | ||
context('EDGE CASE: use td.callback() as a matcher with a thenCallback chain (callback() wins)', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback('lolz'))).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.er === 'lolz'; | ||
}); | ||
return And(function() { | ||
return this.results === void 0; | ||
}); | ||
}); | ||
context('EDGE CASE: Multiple td.callbacks, some markers and some matchers', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/bar', td.callback('neat'), td.callback, 'hi')).thenCallback('perfect'); | ||
}); | ||
return describe('when', function() { | ||
context('callback is synchronous', function() { | ||
When(function() { | ||
return this.testDouble('/bar', ((function(_this) { | ||
return function(cb1arg1) { | ||
_this.cb1arg1 = cb1arg1; | ||
return this.returnValue = this.testDouble('/foo', (function(_this) { | ||
return function(er, results) { | ||
_this.callbackInvoked = true; | ||
_this.er = er; | ||
return _this.results = results; | ||
}; | ||
})(this)), ((function(_this) { | ||
return function(cb2arg1) { | ||
_this.cb2arg1 = cb2arg1; | ||
}; | ||
})(this)), 'hi'); | ||
})(this)); | ||
}); | ||
Then(function() { | ||
return this.cb1arg1 === 'neat'; | ||
context('VERBOSE: using td.callback() as a matcher with a thenReturn chain', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback(null, 'some results'))).thenReturn('pandas'); | ||
}); | ||
Then(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === 'pandas'; | ||
}); | ||
}); | ||
return And(function() { | ||
return this.cb2arg1 === 'perfect'; | ||
context('TERSE: use thenCallback chain with td.callback implied as last arg', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo')).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.callbackInvoked = true; | ||
}); | ||
And(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === void 0; | ||
}); | ||
}); | ||
}); | ||
context('EDGE CASE: use td.callback as a marker with thenReturn (no-arg invocation is made)', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback)).thenReturn(null); | ||
context('ORDER-EXPLICIT: use td.callback as a marker with a thenCallback chain', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback)).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.er === null; | ||
}); | ||
And(function() { | ||
return this.results === 'some results'; | ||
}); | ||
return And(function() { | ||
return this.returnValue === void 0; | ||
}); | ||
}); | ||
Then(function() { | ||
return this.er === void 0; | ||
context('EDGE CASE: use td.callback() as a matcher with a thenCallback chain (callback() wins)', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback('lolz'))).thenCallback(null, 'some results'); | ||
}); | ||
Then(function() { | ||
return this.er === 'lolz'; | ||
}); | ||
return And(function() { | ||
return this.results === void 0; | ||
}); | ||
}); | ||
And(function() { | ||
return this.results === void 0; | ||
context('EDGE CASE: Multiple td.callbacks, some markers and some matchers', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/bar', td.callback('neat'), td.callback, 'hi')).thenCallback('perfect'); | ||
}); | ||
When(function() { | ||
return this.testDouble('/bar', ((function(_this) { | ||
return function(cb1arg1) { | ||
_this.cb1arg1 = cb1arg1; | ||
}; | ||
})(this)), ((function(_this) { | ||
return function(cb2arg1) { | ||
_this.cb2arg1 = cb2arg1; | ||
}; | ||
})(this)), 'hi'); | ||
}); | ||
Then(function() { | ||
return this.cb1arg1 === 'neat'; | ||
}); | ||
return And(function() { | ||
return this.cb2arg1 === 'perfect'; | ||
}); | ||
}); | ||
return And(function() { | ||
return this.callbackInvoked === true; | ||
context('EDGE CASE: use td.callback as a marker with thenReturn (no-arg invocation is made)', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/foo', td.callback)).thenReturn(null); | ||
}); | ||
Then(function() { | ||
return this.er === void 0; | ||
}); | ||
And(function() { | ||
return this.results === void 0; | ||
}); | ||
return And(function() { | ||
return this.callbackInvoked === true; | ||
}); | ||
}); | ||
return context('EDGE CASE: thenCallback used but not satisfied', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/bar')).thenCallback('a-ha'); | ||
}); | ||
Given(function() { | ||
return td.when(this.testDouble('/bar')).thenReturn('o_O'); | ||
}); | ||
When(function() { | ||
return this.result = this.testDouble('/bar'); | ||
}); | ||
return Then(function() { | ||
return this.result === 'o_O'; | ||
}); | ||
}); | ||
}); | ||
return context('EDGE CASE: thenCallback used but not satisfied', function() { | ||
Given(function() { | ||
return td.when(this.testDouble('/bar')).thenCallback('a-ha'); | ||
return context('callback is asynchronous', function() { | ||
describe('using the defer option', function() { | ||
it('does not invoke synchronously', function(done) { | ||
td.when(this.testDouble('/A'), { | ||
defer: true | ||
}).thenCallback(null, 'B'); | ||
this.testDouble('/A', (function(_this) { | ||
return function(er, result) { | ||
_this.callbackInvoked = true; | ||
_this.result = result; | ||
return done(); | ||
}; | ||
})(this)); | ||
if (this.result != null) { | ||
return this.invokedSynchronously = true; | ||
} | ||
}); | ||
return afterEach(function() { | ||
expect(this.callbackInvoked).to.eq(true); | ||
expect(this.result).to.eq('B'); | ||
return expect(this.invokedSynchronously).not.to.eq(true); | ||
}); | ||
}); | ||
Given(function() { | ||
return td.when(this.testDouble('/bar')).thenReturn('o_O'); | ||
return describe('using the delay option', function() { | ||
if (typeof Promise !== 'function') { | ||
return; | ||
} | ||
it('wraps callbacks and promises in the right order', function(done) { | ||
td.when(this.testDouble('/A'), { | ||
delay: 20 | ||
}).thenCallback(null, 'B'); | ||
td.when(this.testDouble('/C'), { | ||
delay: 10 | ||
}).thenCallback(null, 'D'); | ||
td.when(this.testDouble('/E'), { | ||
delay: 15 | ||
}).thenResolve('F'); | ||
td.when(this.testDouble('/G'), { | ||
delay: 5 | ||
}).thenReject('H'); | ||
this.results = []; | ||
this.testDouble('/A', (function(_this) { | ||
return function(er, result) { | ||
_this.results.push(result); | ||
if (_this.results.length === 4) { | ||
return done(); | ||
} | ||
}; | ||
})(this)); | ||
this.testDouble('/C', (function(_this) { | ||
return function(er, result) { | ||
_this.results.push(result); | ||
if (_this.results.length === 4) { | ||
return done(); | ||
} | ||
}; | ||
})(this)); | ||
this.testDouble('/E').then((function(_this) { | ||
return function(result) { | ||
_this.results.push(result); | ||
if (_this.results.length === 4) { | ||
return done(); | ||
} | ||
}; | ||
})(this)); | ||
this.testDouble('/G')["catch"]((function(_this) { | ||
return function(error) { | ||
_this.results.push(error); | ||
if (_this.results.length === 4) { | ||
return done(); | ||
} | ||
}; | ||
})(this)); | ||
if (this.results.length > 0) { | ||
return this.invokedSynchronously = true; | ||
} | ||
}); | ||
return afterEach(function() { | ||
expect(this.results).to.deep.eq(['H', 'D', 'F', 'B']); | ||
return expect(this.invokedSynchronously).not.to.eq(true); | ||
}); | ||
}); | ||
When(function() { | ||
return this.result = this.testDouble('/bar'); | ||
}); | ||
return Then(function() { | ||
return this.result === 'o_O'; | ||
}); | ||
}); | ||
}); | ||
return describe('verify???? what would that mean', function() {}); | ||
}); | ||
}).call(this); |
// Generated by CoffeeScript 1.12.4 | ||
(function() { | ||
var _, argsMatch, callback, callsStore, config, ensurePromise, executePlan, hasTimesRemaining, invokeCallbackFor, isSatisfied, log, store, stubbedValueFor, stubbingFor; | ||
var _, argsMatch, callCallback, callback, callbackArgs, callsStore, config, createPromise, ensurePromise, executePlan, hasTimesRemaining, invokeCallbackFor, isSatisfied, log, store, stubbedValueFor, stubbingFor, | ||
slice = [].slice; | ||
@@ -47,4 +48,3 @@ _ = require('../util/lodash-wrap'); | ||
executePlan = function(stubbing, actualArgs) { | ||
var Promise, value; | ||
Promise = config().promiseConstructor; | ||
var value; | ||
value = stubbedValueFor(stubbing); | ||
@@ -62,11 +62,5 @@ stubbing.callCount += 1; | ||
case "thenResolve": | ||
ensurePromise(Promise); | ||
return new Promise(function(resolve) { | ||
return resolve(value); | ||
}); | ||
return createPromise(stubbing, value, true); | ||
case "thenReject": | ||
ensurePromise(Promise); | ||
return new Promise(function(resolve, reject) { | ||
return reject(value); | ||
}); | ||
return createPromise(stubbing, value, false); | ||
} | ||
@@ -80,11 +74,46 @@ }; | ||
return _.each(stubbing.args, function(expectedArg, i) { | ||
var callbackArgs; | ||
var args; | ||
if (!callback.isCallback(expectedArg)) { | ||
return; | ||
} | ||
callbackArgs = expectedArg.args != null ? expectedArg.args : stubbing.config.plan === 'thenCallback' ? stubbing.stubbedValues : []; | ||
return actualArgs[i].apply(actualArgs, callbackArgs); | ||
args = callbackArgs(stubbing, expectedArg); | ||
return callCallback(stubbing, actualArgs[i], args); | ||
}); | ||
}; | ||
callbackArgs = function(stubbing, expectedArg) { | ||
if (expectedArg.args != null) { | ||
return expectedArg.args; | ||
} else if (stubbing.config.plan === 'thenCallback') { | ||
return stubbing.stubbedValues; | ||
} else { | ||
return []; | ||
} | ||
}; | ||
callCallback = function(stubbing, callback, args) { | ||
if (stubbing.config.delay) { | ||
return _.delay.apply(_, [callback, stubbing.config.delay].concat(slice.call(args))); | ||
} else if (stubbing.config.defer) { | ||
return _.defer.apply(_, [callback].concat(slice.call(args))); | ||
} else { | ||
return callback.apply(null, args); | ||
} | ||
}; | ||
createPromise = function(stubbing, value, willResolve) { | ||
var Promise; | ||
Promise = config().promiseConstructor; | ||
ensurePromise(Promise); | ||
return new Promise(function(resolve, reject) { | ||
return callCallback(stubbing, function() { | ||
if (willResolve) { | ||
return resolve(value); | ||
} else { | ||
return reject(value); | ||
} | ||
}, [value]); | ||
}); | ||
}; | ||
stubbedValueFor = function(stubbing) { | ||
@@ -91,0 +120,0 @@ if (stubbing.callCount < stubbing.stubbedValues.length) { |
@@ -27,2 +27,14 @@ 'use strict'; | ||
}); | ||
Object.defineProperty(exports, 'delay', { | ||
enumerable: true, | ||
get: function get() { | ||
return _lodash.delay; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'defer', { | ||
enumerable: true, | ||
get: function get() { | ||
return _lodash.defer; | ||
} | ||
}); | ||
Object.defineProperty(exports, 'each', { | ||
@@ -29,0 +41,0 @@ enumerable: true, |
// Generated by CoffeeScript 1.12.4 | ||
(function() { | ||
module.exports = '2.0.0-pre.4'; | ||
module.exports = '2.0.0-pre.5'; | ||
}).call(this); |
{ | ||
"name": "testdouble", | ||
"version": "2.0.0-pre.4", | ||
"version": "2.0.0-pre.5", | ||
"description": "A minimal test double library for TDD with JavaScript", | ||
@@ -5,0 +5,0 @@ "homepage": "https://github.com/testdouble/testdouble.js", |
@@ -112,2 +112,4 @@ # testdouble.js | ||
2. [times](docs/5-stubbing-results.md#times) | ||
3. [defer](docs/5-stubbing-results.md#defer) | ||
4. [delay](docs/5-stubbing-results.md#delay) | ||
6. [Verifying invocations](docs/6-verifying-invocations.md#verifying-interactions) | ||
@@ -114,0 +116,0 @@ 1. [td.verify() API](docs/6-verifying-invocations.md#tdverify) |
@@ -5,2 +5,4 @@ export { | ||
clone, | ||
delay, | ||
defer, | ||
each, | ||
@@ -7,0 +9,0 @@ every, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
1449371
26850
143