diff --git a/docs/release-source/release/migrating-to-2.0.md b/docs/release-source/release/migrating-to-2.0.md index be93c2717..ffe520f3c 100644 --- a/docs/release-source/release/migrating-to-2.0.md +++ b/docs/release-source/release/migrating-to-2.0.md @@ -53,3 +53,7 @@ The following utility functions are being marked as deprecated and are planned f ## `sandbox.useFakeXMLHttpRequest` no longer returns a "server" In Sinon 1.x, the sandbox' `useFakeXMLHttpRequest` was the same as it's `useFakeServer`. In 2.x, it maps directly to `sinon.useFakeXMLHttpRequest` (but with sandboxing). If you use `sandbox.useFakeXMLHttpRequest`, just replace it with `sandbox.useFakeServer`, and your tests should behave as they always did. + +## `sinon.behavior` is gone + +The `sinon.behavior` object is no longer exposed for random modification. However, there is a new mechanism in place aided to add new behavior to stubs, `sinon.addBehavior(name, fn)`, see the stub docs. diff --git a/docs/release-source/release/stubs.md b/docs/release-source/release/stubs.md index 0891812bf..214b05347 100644 --- a/docs/release-source/release/stubs.md +++ b/docs/release-source/release/stubs.md @@ -434,3 +434,17 @@ Same as their corresponding non-Async counterparts, but with callback being defe #### `stub.yieldsToOnAsync(property, context, [arg1, arg2, ...])` Same as their corresponding non-Async counterparts, but with callback being deferred (executed not immediately but after short timeout and in another "thread") + +#### `sinon.addBehavior(name, fn);` + +Add a custom behavior. The name will be available as a function on stubs, and the chaining mechanism will be set up for you (e.g. no need to return anything from your function, it's return value will be ignored). The `fn` will be passed the fake instance as its first argument, and then the user's arguments. + +```javascript +const sinon = require('sinon'); + +sinon.addBehavior('returnsNum', (fake, n) => fake.returns(n)); + +var stub = sinon.stub().returnsNum(42); + +assert.equals(stub(), 42); +``` diff --git a/lib/sinon.js b/lib/sinon.js index cc4f420b0..bbac254db 100644 --- a/lib/sinon.js +++ b/lib/sinon.js @@ -64,3 +64,8 @@ exports.useFakeXMLHttpRequest = fakeXhr.useFakeXMLHttpRequest; exports.fakeServer = require("./sinon/util/fake_server"); exports.fakeServerWithClock = require("./sinon/util/fake_server_with_clock"); +var behavior = require("./sinon/behavior"); + +exports.addBehavior = function (name, fn) { + behavior.addBehavior(exports.stub, name, fn); +}; diff --git a/lib/sinon/behavior.js b/lib/sinon/behavior.js index c6b3653de..42112d778 100644 --- a/lib/sinon/behavior.js +++ b/lib/sinon/behavior.js @@ -23,19 +23,6 @@ var nextTick = (function () { }; })(); -function throwsException(error, message) { - if (typeof error === "string") { - this.exception = new Error(message || ""); - this.exception.name = error; - } else if (!error) { - this.exception = new Error("Error"); - } else { - this.exception = error; - } - - return this.chain(); -} - function getCallback(behavior, args) { var callArgAt = behavior.callArgAt; @@ -114,6 +101,8 @@ var proto = { create: function create(stub) { var behavior = extend({}, proto); delete behavior.create; + delete behavior.addBehavior; + delete behavior.createBehavior; behavior.stub = stub; return behavior; @@ -173,183 +162,6 @@ var proto = { ); }, - callsFake: function callsFake(fn) { - this.fakeFn = fn; - return this.chain(); - }, - - callsArg: function callsArg(pos) { - if (typeof pos !== "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = []; - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - callsArgOn: function callsArgOn(pos, context) { - if (typeof pos !== "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = []; - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - callsArgWith: function callsArgWith(pos) { - if (typeof pos !== "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - callsArgOnWith: function callsArgWith(pos, context) { - if (typeof pos !== "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 2); - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - yields: function () { - this.callArgAt = useLeftMostCallback; - this.callbackArguments = slice.call(arguments, 0); - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - yieldsRight: function () { - this.callArgAt = useRightMostCallback; - this.callbackArguments = slice.call(arguments, 0); - this.callbackContext = undefined; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - yieldsOn: function (context) { - this.callArgAt = useLeftMostCallback; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = context; - this.callArgProp = undefined; - this.callbackAsync = false; - - return this.chain(); - }, - - yieldsTo: function (prop) { - this.callArgAt = useLeftMostCallback; - this.callbackArguments = slice.call(arguments, 1); - this.callbackContext = undefined; - this.callArgProp = prop; - this.callbackAsync = false; - - return this.chain(); - }, - - yieldsToOn: function (prop, context) { - this.callArgAt = useLeftMostCallback; - this.callbackArguments = slice.call(arguments, 2); - this.callbackContext = context; - this.callArgProp = prop; - this.callbackAsync = false; - - return this.chain(); - }, - - throws: throwsException, - throwsException: throwsException, - - returns: function returns(value) { - this.returnValue = value; - this.resolve = false; - this.reject = false; - this.returnValueDefined = true; - this.exception = undefined; - this.fakeFn = undefined; - - return this.chain(); - }, - - returnsArg: function returnsArg(pos) { - if (typeof pos !== "number") { - throw new TypeError("argument index is not number"); - } - - this.returnArgAt = pos; - - return this.chain(); - }, - - returnsThis: function returnsThis() { - this.returnThis = true; - - return this.chain(); - }, - - resolves: function resolves(value) { - this.returnValue = value; - this.resolve = true; - this.reject = false; - this.returnValueDefined = true; - this.exception = undefined; - this.fakeFn = undefined; - - return this.chain(); - }, - - rejects: function rejects(error, message) { - var reason; - if (typeof error === "string") { - reason = new Error(message || ""); - reason.name = error; - } else if (!error) { - reason = new Error("Error"); - } else { - reason = error; - } - this.returnValue = reason; - this.resolve = false; - this.reject = true; - this.returnValueDefined = true; - this.exception = undefined; - this.fakeFn = undefined; - - return this; - }, - - callThrough: function callThrough() { - this.callsThrough = true; - return this.chain(); - }, - chain: function chain() { /** * "this" is stub when method is called directly on stub, e.g. stub.returns(123); @@ -375,4 +187,23 @@ Object.keys(proto).forEach(function (method) { } }); +function createBehavior(behaviorMethod) { + return function () { + this.defaultBehavior = this.defaultBehavior || proto.create(this); + this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments); + return this; + }; +} + +function addBehavior(stub, name, fn) { + proto[name] = function () { + fn.apply(this, [this].concat([].slice.call(arguments))); + return this.stub || this; + }; + + stub[name] = createBehavior(name); +} + +proto.addBehavior = addBehavior; +proto.createBehavior = createBehavior; module.exports = proto; diff --git a/lib/sinon/default-behaviors.js b/lib/sinon/default-behaviors.js new file mode 100644 index 000000000..263c500ca --- /dev/null +++ b/lib/sinon/default-behaviors.js @@ -0,0 +1,183 @@ +"use strict"; + +var slice = [].slice; +var useLeftMostCallback = -1; +var useRightMostCallback = -2; + +function throwsException(fake, error, message) { + if (typeof error === "string") { + fake.exception = new Error(message || ""); + fake.exception.name = error; + } else if (!error) { + fake.exception = new Error("Error"); + } else { + fake.exception = error; + } +} + +module.exports = { + callsFake: function callsFake(fake, fn) { + fake.fakeFn = fn; + }, + + callsArg: function callsArg(fake, pos) { + if (typeof pos !== "number") { + throw new TypeError("argument index is not number"); + } + + fake.callArgAt = pos; + fake.callbackArguments = []; + fake.callbackContext = undefined; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + callsArgOn: function callsArgOn(fake, pos, context) { + if (typeof pos !== "number") { + throw new TypeError("argument index is not number"); + } + + fake.callArgAt = pos; + fake.callbackArguments = []; + fake.callbackContext = context; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + callsArgWith: function callsArgWith(fake, pos) { + if (typeof pos !== "number") { + throw new TypeError("argument index is not number"); + } + + fake.callArgAt = pos; + fake.callbackArguments = slice.call(arguments, 2); + fake.callbackContext = undefined; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + callsArgOnWith: function callsArgWith(fake, pos, context) { + if (typeof pos !== "number") { + throw new TypeError("argument index is not number"); + } + + fake.callArgAt = pos; + fake.callbackArguments = slice.call(arguments, 3); + fake.callbackContext = context; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + yields: function (fake) { + fake.callArgAt = useLeftMostCallback; + fake.callbackArguments = slice.call(arguments, 1); + fake.callbackContext = undefined; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + yieldsRight: function (fake) { + fake.callArgAt = useRightMostCallback; + fake.callbackArguments = slice.call(arguments, 1); + fake.callbackContext = undefined; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + yieldsOn: function (fake, context) { + fake.callArgAt = useLeftMostCallback; + fake.callbackArguments = slice.call(arguments, 2); + fake.callbackContext = context; + fake.callArgProp = undefined; + fake.callbackAsync = false; + }, + + yieldsTo: function (fake, prop) { + fake.callArgAt = useLeftMostCallback; + fake.callbackArguments = slice.call(arguments, 2); + fake.callbackContext = undefined; + fake.callArgProp = prop; + fake.callbackAsync = false; + }, + + yieldsToOn: function (fake, prop, context) { + fake.callArgAt = useLeftMostCallback; + fake.callbackArguments = slice.call(arguments, 3); + fake.callbackContext = context; + fake.callArgProp = prop; + fake.callbackAsync = false; + }, + + throws: throwsException, + throwsException: throwsException, + + returns: function returns(fake, value) { + fake.returnValue = value; + fake.resolve = false; + fake.reject = false; + fake.returnValueDefined = true; + fake.exception = undefined; + fake.fakeFn = undefined; + }, + + returnsArg: function returnsArg(fake, pos) { + if (typeof pos !== "number") { + throw new TypeError("argument index is not number"); + } + + fake.returnArgAt = pos; + }, + + returnsThis: function returnsThis(fake) { + fake.returnThis = true; + }, + + resolves: function resolves(fake, value) { + fake.returnValue = value; + fake.resolve = true; + fake.reject = false; + fake.returnValueDefined = true; + fake.exception = undefined; + fake.fakeFn = undefined; + }, + + rejects: function rejects(fake, error, message) { + var reason; + if (typeof error === "string") { + reason = new Error(message || ""); + reason.name = error; + } else if (!error) { + reason = new Error("Error"); + } else { + reason = error; + } + fake.returnValue = reason; + fake.resolve = false; + fake.reject = true; + fake.returnValueDefined = true; + fake.exception = undefined; + fake.fakeFn = undefined; + + return fake; + }, + + callThrough: function callThrough(fake) { + fake.callsThrough = true; + } +}; + +function createAsyncVersion(syncFnName) { + return function () { + var result = module.exports[syncFnName].apply(this, arguments); + this.callbackAsync = true; + return result; + }; +} + +// create asynchronous versions of callsArg* and yields* methods +Object.keys(module.exports).forEach(function (method) { + // need to avoid creating anotherasync versions of the newly added async methods + if (method.match(/^(callsArg|yields)/) && !method.match(/Async/)) { + module.exports[method + "Async"] = createAsyncVersion(method); + } +}); diff --git a/lib/sinon/stub.js b/lib/sinon/stub.js index 79e23a652..7580dda9f 100644 --- a/lib/sinon/stub.js +++ b/lib/sinon/stub.js @@ -1,6 +1,7 @@ "use strict"; var behavior = require("./behavior"); +var behaviors = require("./default-behaviors"); var spy = require("./spy"); var extend = require("./util/core/extend"); var functionToString = require("./util/core/function-to-string"); @@ -127,24 +128,21 @@ var proto = { } }; -function createBehavior(behaviorMethod) { - return function () { - this.defaultBehavior = this.defaultBehavior || behavior.create(this); - this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments); - return this; - }; -} - Object.keys(behavior).forEach(function (method) { if (behavior.hasOwnProperty(method) && !proto.hasOwnProperty(method) && method !== "create" && method !== "withArgs" && method !== "invoke") { - proto[method] = createBehavior(method); + proto[method] = behavior.createBehavior(method); } }); -extend(stub, proto); +Object.keys(behaviors).forEach(function (method) { + if (behaviors.hasOwnProperty(method) && !proto.hasOwnProperty(method)) { + behavior.addBehavior(stub, method, behaviors[method]); + } +}); +extend(stub, proto); module.exports = stub; diff --git a/test/behavior-test.js b/test/behavior-test.js new file mode 100644 index 000000000..c92719c74 --- /dev/null +++ b/test/behavior-test.js @@ -0,0 +1,17 @@ +"use strict"; + +var createStub = require("../lib/sinon/stub"); +var addBehavior = require("../lib/sinon").addBehavior; +var assert = require("referee").assert; + +describe("behaviors", function () { + it("adds and uses a custom behavior", function () { + addBehavior("returnsNum", function (fake, n) { + fake.returns(n); + }); + + var stub = createStub().returnsNum(42); + + assert.equals(stub(), 42); + }); +});