Testing routers in backbone.js properly?

偶尔善良 提交于 2019-12-02 15:33:35

Here's a low-levelish way of doing it with jasmine, testing that pushState works as expected and that your router sets up things properly... I assume a router that has been initialized and has a home route mapped to ''. You can adapt this for your other routes. I also assume you've done in your app initialization a Backbone.history.start({ pushState: true });

    describe('app.Router', function () {

        var router = app.router, pushStateSpy;

        it('has a "home" route', function () {
            expect(router.routes['']).toEqual('home');
        });

        it('triggers the "home" route', function () {
            var home = spyOn(router, 'home').andCallThrough();
            pushStateSpy = spyOn(window.history, 'pushState').andCallFake(function (data, title, url) {
                expect(url).toEqual('/');
                router.home();
            });
            router.navigate('');
            expect(pushStateSpy).toHaveBeenCalled();
            expect(home).toHaveBeenCalled();
            ...
        });
    });  

You can effectively achieve similar things by doing Backbone.history.stop(); it's meant for this reason.

UPDATE: Browsers with no pushState:

This of course will work fine if your browser you test on has support for pushState. If you test against browsers that don't, you can conditionally test as follows:

it('triggers the "home" route', function () {
    var home = spyOn(router, 'home').andCallThrough();

    if (Backbone.history._hasPushState) {
        pushStateSpy = spyOn(window.history, 'pushState').andCallFake(function (data, title, url) {
            expect(url).toEqual('/');
            router.home();
        });
        router.navigate('', {trigger: true});
        expect(pushStateSpy).toHaveBeenCalled();
        expect(home).toHaveBeenCalled();

    } else if (Backbone.history._wantsHashChange) {
        var updateHashSpy = spyOn(Backbone.history, '_updateHash').andCallFake(function (loc, frag) {
            expect(frag).toEqual('');
            router.home();
        });
        router.navigate('', {trigger: true});
        expect(updateHashSpy).toHaveBeenCalled();
        expect(home).toHaveBeenCalled();
    }
});

If you are on IE6, good luck.

When I'm testing a backbone router, what I care about is that the routes I provided are invoking the functions I specify with the correct arguments. A lot of the other answers here aren't really testing that.

If you need to test the functionality of some routes, you can test those functions by themselves.

Assuming you have a simple router:

App.Router = Backbone.Router.extend({
  routes: {
    '(/)':'index',
    '/item/:id':'item'
  },
  index: {
    //render some template
  }, 
  item: {
    //render some other template, or redirect, or _whatever_
  }
});

Here's how I do it:

describe('Router', function() {

  var trigger = {trigger: true};
  var router

  beforeEach(function() {
    // This is the trick, right here:
    // The Backbone history code dodges our spies
    // unless we set them up exactly like this:
    Backbone.history.stop(); //stop the router
    spyOn(Router.prototype, 'index'); //spy on our routes, and they won't get called
    spyOn(Router.prototype, 'route2'); 

    router = new App.Router(); // Set up the spies _before_ creating the router
    Backbone.history.start();
  });

  it('empty route routes to index', function(){
    Backbone.history.navigate('', trigger);
    expect(router.index).toHaveBeenCalled();
  });

  it('/ routes to index', function(){
    router.navigate('/', trigger);
    expect(router.index).toHaveBeenCalled();
  });

  it('/item routes to item with id', function(){
    router.navigate('/item/someId', trigger);
    expect(router.item).toHaveBeenCalledWith('someId');
  });
});

Here's what I ended up using myself. I made a mock version of the router by extending it and overriding the methods with a blank method to prevent it from invoking any further logic when being called:

describe("routers/main", function() {

    beforeEach(function() {

        // Create a mock version of our router by extending it and only overriding
        // the methods
        var mockRouter = App.Routers["Main"].extend({
            index: function() {},
            login: function() {},
            logoff: function() {}
        });

        // Set up a spy and invoke the router
        this.routeSpy = sinon.spy();
        this.router = new mockRouter;

        // Prevent history.start from throwing error
        try {
            Backbone.history.start({silent:true, pushState:true});
        } catch(e) {

        }

        // Reset URL
        this.router.navigate("tests/SpecRunner.html");
    });

    afterEach(function(){
        // Reset URL
        this.router.navigate("tests/SpecRunner.html");
    });

    it('Has the right amount of routes', function() {
        expect(_.size(this.router.routes)).toEqual(4);
    });

    it('/ -route exists and points to the right method', function () {
        expect(this.router.routes['']).toEqual('index');
    });

    it("Can navigate to /", function() {
        this.router.bind("route:index", this.routeSpy);
        this.router.navigate("", true);
        expect(this.routeSpy.calledOnce).toBeTruthy();
        expect(this.routeSpy.calledWith()).toBeTruthy();
    });

});

Note that sinon.js is used above to create the spy, along with underscore.js to provide the size function.

There is a very good tutorial about testing backbone:

http://tinnedfruit.com/2011/04/26/testing-backbone-apps-with-jasmine-sinon-3.html

You have to mock Backbone.Router.route which is the function that is internally used to bind the functions on to Backbone.History.

Thats the original function:

route : function(route, name, callback) {
  Backbone.history || (Backbone.history = new Backbone.History);
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
  Backbone.history.route(route, _.bind(function(fragment) {
    var args = this._extractParameters(route, fragment);
    callback.apply(this, args);
    this.trigger.apply(this, ['route:' + name].concat(args));
  }, this));
}

you could to something like this, which simply call the functions when the router will be initialized:

Backbone.Router.route = function(route, name, callback) {
    callback();
}

You could also save the callbacks in a object and with the route as name and call same steps by step:

var map = {}
Backbone.Router.route = function(route, name, callback) {
    map[route] = callback();
}

for(i in map){
    map[i]();
}
Andy Armstrong

I started out using ggozad's solution of spying on _updateHash which partially worked for me. However, I discovered that my tests were confused because the hash never updated, so code that relied upon calls to getHash or getFragment were failing.

What I ended up with is the following helper function that spies on both _updateHash and getHash. The former records the request to update the hash, and the latter returns the last hash that was passed to _updateHash. I call this helper function in my tests before I start the Backbone history.

    /**
     * Prevent Backbone tests from changing the browser's URL.
     *
     * This function modifies Backbone so that tests can navigate
     * without modifying the browser's URL. It works be adding
     * stub versions of Backbone's hash functions so that updating
     * the hash doesn't change the URL but instead updates a
     * local object. The router's callbacks are still invoked
     * so that to the test it appears that navigation is behaving
     * as expected.
     *
     * Note: it is important that tests don't update the browser's
     * URL because subsequent tests could find themselves in an
     * unexpected navigation state.
     */
    preventBackboneChangingUrl = function() {
        var history = {
            currentFragment: ''
        };

        // Stub out the Backbone router so that the browser doesn't actually navigate
        spyOn(Backbone.history, '_updateHash').andCallFake(function (location, fragment, replace) {
            history.currentFragment = fragment;
        });

        // Stub out getHash so that Backbone thinks that the browser has navigated
        spyOn(Backbone.history, 'getHash').andCallFake(function () {
            return history.currentFragment;
        });
    };
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!