Testing routers in backbone.js properly?

前端 未结 6 916
太阳男子
太阳男子 2021-01-30 13:56

So I\'ve just started to write tests for my in-progress javascript app, using sinon.js & jasmine.js. Works pretty well overall, but I need to also

相关标签:
6条回答
  • 2021-01-30 14:19

    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.

    0 讨论(0)
  • 2021-01-30 14:22

    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]();
    }
    
    0 讨论(0)
  • 2021-01-30 14:23

    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');
      });
    });
    
    0 讨论(0)
  • 2021-01-30 14:25

    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.

    0 讨论(0)
  • 2021-01-30 14:41

    There is a very good tutorial about testing backbone:

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

    0 讨论(0)
  • 2021-01-30 14:42

    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;
            });
        };
    
    0 讨论(0)
提交回复
热议问题