Skip to content

Commit

Permalink
Give addBehavior a reasonable API
Browse files Browse the repository at this point in the history
The initial implementation forced behaviors to do things to `this`, which is
terrible API design (`this` being a dynamic/unstable implicit argument). This
commit passes the fake as the first explicit argument to behaviors instead,
allowing for a clean contract.
  • Loading branch information
cjohansen committed Feb 26, 2017
1 parent c404d20 commit ba00de0
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 100 deletions.
8 changes: 3 additions & 5 deletions docs/release-source/release/stubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,16 +437,14 @@ Same as their corresponding non-Async counterparts, but with callback being defe

#### `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. Inside the `fn`, `this` is bound to the fake.
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('returns42', function () {
this.returns(42);
});
sinon.addBehavior('returnsNum', (fake, n) => fake.returns(n));

var stub = sinon.stub().returns42();
var stub = sinon.stub().returnsNum(42);

assert.equals(stub(), 42);
```
2 changes: 1 addition & 1 deletion lib/sinon/behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function createBehavior(behaviorMethod) {

function addBehavior(stub, name, fn) {
proto[name] = function () {
fn.apply(this, arguments);
fn.apply(this, [this].concat([].slice.call(arguments)));
return this.stub || this;
};

Expand Down
182 changes: 91 additions & 91 deletions lib/sinon/default-behaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,144 +4,144 @@ var slice = [].slice;
var useLeftMostCallback = -1;
var useRightMostCallback = -2;

function throwsException(error, message) {
function throwsException(fake, error, message) {
if (typeof error === "string") {
this.exception = new Error(message || "");
this.exception.name = error;
fake.exception = new Error(message || "");
fake.exception.name = error;
} else if (!error) {
this.exception = new Error("Error");
fake.exception = new Error("Error");
} else {
this.exception = error;
fake.exception = error;
}
}

module.exports = {
callsFake: function callsFake(fn) {
this.fakeFn = fn;
callsFake: function callsFake(fake, fn) {
fake.fakeFn = fn;
},

callsArg: function callsArg(pos) {
callsArg: function callsArg(fake, 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;
fake.callArgAt = pos;
fake.callbackArguments = [];
fake.callbackContext = undefined;
fake.callArgProp = undefined;
fake.callbackAsync = false;
},

callsArgOn: function callsArgOn(pos, context) {
callsArgOn: function callsArgOn(fake, 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;
fake.callArgAt = pos;
fake.callbackArguments = [];
fake.callbackContext = context;
fake.callArgProp = undefined;
fake.callbackAsync = false;
},

callsArgWith: function callsArgWith(pos) {
callsArgWith: function callsArgWith(fake, 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;
fake.callArgAt = pos;
fake.callbackArguments = slice.call(arguments, 2);
fake.callbackContext = undefined;
fake.callArgProp = undefined;
fake.callbackAsync = false;
},

callsArgOnWith: function callsArgWith(pos, context) {
callsArgOnWith: function callsArgWith(fake, 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;
fake.callArgAt = pos;
fake.callbackArguments = slice.call(arguments, 3);
fake.callbackContext = context;
fake.callArgProp = undefined;
fake.callbackAsync = false;
},

yields: function () {
this.callArgAt = useLeftMostCallback;
this.callbackArguments = slice.call(arguments, 0);
this.callbackContext = undefined;
this.callArgProp = undefined;
this.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 () {
this.callArgAt = useRightMostCallback;
this.callbackArguments = slice.call(arguments, 0);
this.callbackContext = undefined;
this.callArgProp = undefined;
this.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 (context) {
this.callArgAt = useLeftMostCallback;
this.callbackArguments = slice.call(arguments, 1);
this.callbackContext = context;
this.callArgProp = undefined;
this.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 (prop) {
this.callArgAt = useLeftMostCallback;
this.callbackArguments = slice.call(arguments, 1);
this.callbackContext = undefined;
this.callArgProp = prop;
this.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 (prop, context) {
this.callArgAt = useLeftMostCallback;
this.callbackArguments = slice.call(arguments, 2);
this.callbackContext = context;
this.callArgProp = prop;
this.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(value) {
this.returnValue = value;
this.resolve = false;
this.reject = false;
this.returnValueDefined = true;
this.exception = undefined;
this.fakeFn = undefined;
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(pos) {
returnsArg: function returnsArg(fake, pos) {
if (typeof pos !== "number") {
throw new TypeError("argument index is not number");
}

this.returnArgAt = pos;
fake.returnArgAt = pos;
},

returnsThis: function returnsThis() {
this.returnThis = true;
returnsThis: function returnsThis(fake) {
fake.returnThis = true;
},

resolves: function resolves(value) {
this.returnValue = value;
this.resolve = true;
this.reject = false;
this.returnValueDefined = true;
this.exception = undefined;
this.fakeFn = undefined;
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(error, message) {
rejects: function rejects(fake, error, message) {
var reason;
if (typeof error === "string") {
reason = new Error(message || "");
Expand All @@ -151,24 +151,24 @@ module.exports = {
} else {
reason = error;
}
this.returnValue = reason;
this.resolve = false;
this.reject = true;
this.returnValueDefined = true;
this.exception = undefined;
this.fakeFn = undefined;

return this;
fake.returnValue = reason;
fake.resolve = false;
fake.reject = true;
fake.returnValueDefined = true;
fake.exception = undefined;
fake.fakeFn = undefined;

return fake;
},

callThrough: function callThrough() {
this.callsThrough = true;
callThrough: function callThrough(fake) {
fake.callsThrough = true;
}
};

function createAsyncVersion(syncFnName) {
return function () {
var result = this[syncFnName].apply(this, arguments);
var result = module.exports[syncFnName].apply(this, arguments);
this.callbackAsync = true;
return result;
};
Expand Down
6 changes: 3 additions & 3 deletions test/behavior-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ var assert = require("referee").assert;

describe("behaviors", function () {
it("adds and uses a custom behavior", function () {
addBehavior("returns42", function () {
this.returns(42);
addBehavior("returnsNum", function (fake, n) {
fake.returns(n);
});

var stub = createStub().returns42();
var stub = createStub().returnsNum(42);

assert.equals(stub(), 42);
});
Expand Down

0 comments on commit ba00de0

Please sign in to comment.