Sinon 存根未被调用
Sinon stubs not been called
在为 baz.View.likeButton
编写测试时,我遇到了一个问题,即当我测试是否已针对我触发的事件调用存根时,这个存根从未被调用。例如,当我直接调用 this.view._update
并期望我的存根 likeStub
或 unLikeStub
也被调用时,也会发生这种情况。我已检查该元素是否存在,但不明白为什么我的期望失败了。
我错过了什么?
我的赞按钮视图继承自 BaseButton
,后者又继承自 base
。
baz.View.likeButton = baz.View.BaseButton.extend({
template: _.template($('#like-button').html()),
sPaper: null,
sPolyFill: null,
sPolyEmpty: null,
liked: null,
likeButn: null,
model: null,
events: {
"click button.icon": "toggleLike"
},
initialize: function (options) {
// inherit from BaseButton
View.BaseButton.prototype.initialize.apply(this, [options]); // inherit from BaseButtonView
this.model = options.model;
this.liked = this.model.get('liked');
// set initial variables
this.likeButn = this.$("button.icon", this);
// pass in el on instantiation
this.el = options.el;
this.svgNode = this.likeButn.find("svg").get(0); // find the svg in the likeButn and get its first object
this.sPaper = Snap(this.svgNode); // pass the svg object into Snap.js
this.sPolyFill = this.sPaper.select('.symbol-solid');
this.sPolyEmpty = this.sPaper.select('.symbol-empty');
this.likeButn.addClass(this.liked ? "liked": "unliked");
this._update();
if(this.model)
{
this.listenTo(this.model, "change:liked", _.bind(this._modelChange, this))
}
},
render: function () {
this.$el.html(this.template());
this._update();
return this;
},
toggleLike: function () {
this.liked = !this.liked;
this._update();
},
_modelChange: function() {
var bNewState = this.model.get('liked');
if(bNewState !== this.liked)
{
this.liked = bNewState;
this._update();
}
},
_update: function () {
if ( this.liked === true ) { // if liked is false, remove class, add class and set isLiked to true, then animate svg to liked position
this._like();
} else if ( this.liked === false ) { // is liked is false, remove class, add class, set isLiked to false, then animate svg to unliked position
this._unlike();
}
},
_like: function() {
this.likeButn.removeClass("unliked");
this.likeButn.addClass("liked");
this.animateLike();
if(this.model.get('liked') !== true)
{
this.model.set('liked', true);
this.model.set("numLikes", this.model.get("numLikes") + 1);
// wrapper for API
likeEntity("/items/"+this.model.id); // should do something better than this to send like to server
}
},
_unlike: function() {
this.likeButn.removeClass("liked");
this.likeButn.addClass("unliked");
this.animateUnlike();
if(this.model.get('liked') !== false)
{
this.model.set('liked', false);
this.model.set("numLikes", this.model.get("numLikes") - 1);
// wrapper for API
unlikeEntity("/items/"+this.model.id); // should do something better than this to send like to server
}
},
animateLike: function () {
this.sPolyFill.animate({ transform: 't9,0' }, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't-9,0' }, 300, mina.easeinout);
},
animateUnlike: function () {
this.sPolyFill.animate({ transform: 't0,0'}, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't0,0' }, 300, mina.easeinout);
}
});
baz.View.base = Backbone.View.extend({
// initialize variables and render template
initialize: function (options) {
this.render(); // render template and initializes above values
},
render: function () {
return this; // A good convention is to return this at the end of render to enable chained calls.
}
});
baz.View.BaseButton = baz.View.base.extend({
// backbone event hash - sets functions to events on selected buttons
events: {
"click button": "triggered"
},
// initialize variables and render template
initialize: function (options) {
baz.View.base.prototype.initialize.apply(this, [options]); // inherit from base
this.btn = this.$("button.normal-buttons");
},
// fn to be called for normal and cancel buttons
triggered: function (event) {
this.trigger('click', this);
this.btn.blur();
}
});
我的测试代码:
describe("Like Button View", function () {
before(function () {
this.$fixture = $('<div id="likeButton-view-fixture"></div>');
});
after(function () {
$("#fixtures").empty();
});
describe("Like button when attribute 'LIKED' is FALSE", function () {
beforeEach(function () {
// set-up fake server
this.server = sinon.fakeServer.create();
this.server.autoRespond = true;
// empty out and rebind the fixture for each run
this.$fixture.empty().appendTo($("#fixtures"));
this.model = new baz.Model.item({
liked: false,
numLikes: 0
});
this.view = new baz.View.likeButton ({
el: this.$fixture,
model: this.model
});
this.renderSpy = sinon.spy(this.view, "render");
this.updateStub = sinon.stub(this.view, "_update");
this.toggleLikeStub = sinon.stub(this.view, "toggleLike");
this.modelChangeStub = sinon.stub(this.view, "_modelChange");
this.likeStub = sinon.stub(this.view, "_like");
this.unlikeStub = sinon.stub(this.view, "_unlike");
this.animateLikeStub = sinon.stub(this.view, "animateLike");
this.animateUnlikeStub = sinon.stub(this.view, "animateUnlike");
this.snapAnimSpy = sinon.spy(Snap, "animate");
});
afterEach(function () {
this.view.model.destroy();
this.renderSpy.restore();
this.updateStub.restore();
this.toggleLikeStub.restore();
this.modelChangeStub.restore();
this.likeStub.restore();
this.unlikeStub.restore();
this.animateLikeStub.restore();
this.animateUnlikeStub.restore();
this.snapAnimSpy.restore();
// undo our server
this.server.restore();
});
it("call render and view should exist", function () {
var _view = this.view.render();
expect(_view).to.equal(this.view);
expect(this.renderSpy).to.have.been.called;
expect(this.view).to.be.ok;
});
it("should have class 'unliked'", function () {
var _element = $(".icon")[0];
expect(_element.className).to.include("unliked");
});
it("trigger 'toggleLike' when button is clicked should change liked value and call update", function () {
var _event = $.Event("click");
var _element = $(".icon");
_element.trigger(_event);
expect(this.toggleLikeStub).to.have.been.called; // this fails
expect(this.view.model.get("liked")).to.deep.equal(true); // this fails
expect(this.updateStub).to.have.been.called; // this fails
});
it("should like an item", function () {
var _element = $(".icon")[0];
this.view._like();
expect(_element.className).to.include("liked");
});
});
});
好的,所以你这里有多个问题。
this.view._update
This is happening too when I directly call this.view._update
for example and expect my stubs likeStub
or unLikeStub
to be called too.
之后你还这样做吗?:
this.updateStub = sinon.stub(this.view, "_update");
toggleLikeStub
我认为这是失败的,因为在您调用 sinon.stub()
之前,原始 toggleLike
被指定为事件处理程序。尝试做这样的事情:
this.updateStub = sinon.stub(baz.View.likeButton.prototype, "_update");
this.view = new baz.View.likeButton ({
el: this.$fixture,
model: this.model
});
你在哪里:
expect(this.updateStub).to.have.been.called; // this fails
updateStub()
不会因为 toggleLikeStub()
被调用而被调用。来自 documentation for sinon.stub()
:
When wrapping an existing function with a stub, the original function is not called.
您确定不需要间谍吗?
其他
我只是想确保你知道,而不是这样做:
this.toggleLikeStub = sinon.stub(this.view, "toggleLike");
// ..
this.toggleLikeStub.restore();
你可以这样做:
sinon.stub(this.view, "toggleLike");
// ..
this.view.toggleLike.restore();
更好的是你可以这样做:
var meths = [
{sinon_meth: 'stub', view_meths: ['_update', 'toggleLike', ...]},
{sinon_meth: 'spy', view_meths: ['render', 'animate', ...]},
];
meths.forEach(function (group) {
group.view_meths.forEach(function (view_meth) {
sinon[group.sinon_meth](view, view_meth);
});
});
// ...
meths.forEach(function (group) {
group.view_meths.forEach(function (view_meth) {
view[view_meth].restore();
});
});
在这种情况下,如果您在每次测试前进行相同的设置并吹走 this.view
,我不确定您是否需要恢复它们。
在为 baz.View.likeButton
编写测试时,我遇到了一个问题,即当我测试是否已针对我触发的事件调用存根时,这个存根从未被调用。例如,当我直接调用 this.view._update
并期望我的存根 likeStub
或 unLikeStub
也被调用时,也会发生这种情况。我已检查该元素是否存在,但不明白为什么我的期望失败了。
我错过了什么?
我的赞按钮视图继承自 BaseButton
,后者又继承自 base
。
baz.View.likeButton = baz.View.BaseButton.extend({
template: _.template($('#like-button').html()),
sPaper: null,
sPolyFill: null,
sPolyEmpty: null,
liked: null,
likeButn: null,
model: null,
events: {
"click button.icon": "toggleLike"
},
initialize: function (options) {
// inherit from BaseButton
View.BaseButton.prototype.initialize.apply(this, [options]); // inherit from BaseButtonView
this.model = options.model;
this.liked = this.model.get('liked');
// set initial variables
this.likeButn = this.$("button.icon", this);
// pass in el on instantiation
this.el = options.el;
this.svgNode = this.likeButn.find("svg").get(0); // find the svg in the likeButn and get its first object
this.sPaper = Snap(this.svgNode); // pass the svg object into Snap.js
this.sPolyFill = this.sPaper.select('.symbol-solid');
this.sPolyEmpty = this.sPaper.select('.symbol-empty');
this.likeButn.addClass(this.liked ? "liked": "unliked");
this._update();
if(this.model)
{
this.listenTo(this.model, "change:liked", _.bind(this._modelChange, this))
}
},
render: function () {
this.$el.html(this.template());
this._update();
return this;
},
toggleLike: function () {
this.liked = !this.liked;
this._update();
},
_modelChange: function() {
var bNewState = this.model.get('liked');
if(bNewState !== this.liked)
{
this.liked = bNewState;
this._update();
}
},
_update: function () {
if ( this.liked === true ) { // if liked is false, remove class, add class and set isLiked to true, then animate svg to liked position
this._like();
} else if ( this.liked === false ) { // is liked is false, remove class, add class, set isLiked to false, then animate svg to unliked position
this._unlike();
}
},
_like: function() {
this.likeButn.removeClass("unliked");
this.likeButn.addClass("liked");
this.animateLike();
if(this.model.get('liked') !== true)
{
this.model.set('liked', true);
this.model.set("numLikes", this.model.get("numLikes") + 1);
// wrapper for API
likeEntity("/items/"+this.model.id); // should do something better than this to send like to server
}
},
_unlike: function() {
this.likeButn.removeClass("liked");
this.likeButn.addClass("unliked");
this.animateUnlike();
if(this.model.get('liked') !== false)
{
this.model.set('liked', false);
this.model.set("numLikes", this.model.get("numLikes") - 1);
// wrapper for API
unlikeEntity("/items/"+this.model.id); // should do something better than this to send like to server
}
},
animateLike: function () {
this.sPolyFill.animate({ transform: 't9,0' }, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't-9,0' }, 300, mina.easeinout);
},
animateUnlike: function () {
this.sPolyFill.animate({ transform: 't0,0'}, 300, mina.easeinout);
this.sPolyEmpty.animate({ transform: 't0,0' }, 300, mina.easeinout);
}
});
baz.View.base = Backbone.View.extend({
// initialize variables and render template
initialize: function (options) {
this.render(); // render template and initializes above values
},
render: function () {
return this; // A good convention is to return this at the end of render to enable chained calls.
}
});
baz.View.BaseButton = baz.View.base.extend({
// backbone event hash - sets functions to events on selected buttons
events: {
"click button": "triggered"
},
// initialize variables and render template
initialize: function (options) {
baz.View.base.prototype.initialize.apply(this, [options]); // inherit from base
this.btn = this.$("button.normal-buttons");
},
// fn to be called for normal and cancel buttons
triggered: function (event) {
this.trigger('click', this);
this.btn.blur();
}
});
我的测试代码:
describe("Like Button View", function () {
before(function () {
this.$fixture = $('<div id="likeButton-view-fixture"></div>');
});
after(function () {
$("#fixtures").empty();
});
describe("Like button when attribute 'LIKED' is FALSE", function () {
beforeEach(function () {
// set-up fake server
this.server = sinon.fakeServer.create();
this.server.autoRespond = true;
// empty out and rebind the fixture for each run
this.$fixture.empty().appendTo($("#fixtures"));
this.model = new baz.Model.item({
liked: false,
numLikes: 0
});
this.view = new baz.View.likeButton ({
el: this.$fixture,
model: this.model
});
this.renderSpy = sinon.spy(this.view, "render");
this.updateStub = sinon.stub(this.view, "_update");
this.toggleLikeStub = sinon.stub(this.view, "toggleLike");
this.modelChangeStub = sinon.stub(this.view, "_modelChange");
this.likeStub = sinon.stub(this.view, "_like");
this.unlikeStub = sinon.stub(this.view, "_unlike");
this.animateLikeStub = sinon.stub(this.view, "animateLike");
this.animateUnlikeStub = sinon.stub(this.view, "animateUnlike");
this.snapAnimSpy = sinon.spy(Snap, "animate");
});
afterEach(function () {
this.view.model.destroy();
this.renderSpy.restore();
this.updateStub.restore();
this.toggleLikeStub.restore();
this.modelChangeStub.restore();
this.likeStub.restore();
this.unlikeStub.restore();
this.animateLikeStub.restore();
this.animateUnlikeStub.restore();
this.snapAnimSpy.restore();
// undo our server
this.server.restore();
});
it("call render and view should exist", function () {
var _view = this.view.render();
expect(_view).to.equal(this.view);
expect(this.renderSpy).to.have.been.called;
expect(this.view).to.be.ok;
});
it("should have class 'unliked'", function () {
var _element = $(".icon")[0];
expect(_element.className).to.include("unliked");
});
it("trigger 'toggleLike' when button is clicked should change liked value and call update", function () {
var _event = $.Event("click");
var _element = $(".icon");
_element.trigger(_event);
expect(this.toggleLikeStub).to.have.been.called; // this fails
expect(this.view.model.get("liked")).to.deep.equal(true); // this fails
expect(this.updateStub).to.have.been.called; // this fails
});
it("should like an item", function () {
var _element = $(".icon")[0];
this.view._like();
expect(_element.className).to.include("liked");
});
});
});
好的,所以你这里有多个问题。
this.view._update
This is happening too when I directly call
this.view._update
for example and expect my stubslikeStub
orunLikeStub
to be called too.之后你还这样做吗?:
this.updateStub = sinon.stub(this.view, "_update");
toggleLikeStub
我认为这是失败的,因为在您调用
sinon.stub()
之前,原始toggleLike
被指定为事件处理程序。尝试做这样的事情:this.updateStub = sinon.stub(baz.View.likeButton.prototype, "_update"); this.view = new baz.View.likeButton ({ el: this.$fixture, model: this.model });
你在哪里:
expect(this.updateStub).to.have.been.called; // this fails
updateStub()
不会因为toggleLikeStub()
被调用而被调用。来自 documentation forsinon.stub()
:When wrapping an existing function with a stub, the original function is not called.
您确定不需要间谍吗?
其他
我只是想确保你知道,而不是这样做:
this.toggleLikeStub = sinon.stub(this.view, "toggleLike"); // .. this.toggleLikeStub.restore();
你可以这样做:
sinon.stub(this.view, "toggleLike"); // .. this.view.toggleLike.restore();
更好的是你可以这样做:
var meths = [ {sinon_meth: 'stub', view_meths: ['_update', 'toggleLike', ...]}, {sinon_meth: 'spy', view_meths: ['render', 'animate', ...]}, ]; meths.forEach(function (group) { group.view_meths.forEach(function (view_meth) { sinon[group.sinon_meth](view, view_meth); }); }); // ... meths.forEach(function (group) { group.view_meths.forEach(function (view_meth) { view[view_meth].restore(); }); });
在这种情况下,如果您在每次测试前进行相同的设置并吹走
this.view
,我不确定您是否需要恢复它们。