Sinon 存根未被调用

Sinon stubs not been called

在为 baz.View.likeButton 编写测试时,我遇到了一个问题,即当我测试是否已针对我触发的事件调用存根时,这个存根从未被调用。例如,当我直接调用 this.view._update 并期望我的存根 likeStubunLikeStub 也被调用时,也会发生这种情况。我已检查该元素是否存在,但不明白为什么我的期望失败了。 我错过了什么?

我的赞按钮视图继承自 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,我不确定您是否需要恢复它们。