Правилно тестване на рутери в backbone.js?

Така че току-що започнах да пиша тестове за моето текущо javascript приложение, използвайки sinon.js & jasmine.js. Работи доста добре като цяло, но трябва да мога да тествам рутерите си.

Рутерите в текущото си състояние ще задействат редица изгледи и други неща, прекратявайки текущия jasmine.js тест чрез извикване на Backbone.navigate в зависимост от състоянието на приложението и действието на потребителския интерфейс.

И така, как бих могъл да тествам дали маршрутизирането до различни местоположения ще работи, като същевременно поддържам рутерите „в пясъчна среда“ и не им позволявам да променят маршрута?

Мога ли да настроя някаква макетна функция, която ще следи промените в pushState или подобни?


person Industrial    schedule 09.02.2012    source източник
comment
Bounty е ON! Очаквам с нетърпение още идеи по този въпрос   -  person Industrial    schedule 13.02.2012


Отговори (6)


Ето един нисък начин да го направите с жасмин, като тествате дали pushState работи според очакванията и дали вашият рутер настройва нещата правилно... Предполагам, че router е инициализиран и има картографиран домашен маршрут да се ''. Можете да адаптирате това за другите си маршрути. Предполагам също, че сте направили в инициализацията на приложението си 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();
            ...
        });
    });  

Можете ефективно да постигнете подобни неща, като направите Backbone.history.stop(); това е предназначено по тази причина.

АКТУАЛИЗАЦИЯ: Браузъри без pushState:

Това разбира се ще работи добре, ако вашият браузър, който тествате, има поддръжка за pushState. Ако тествате срещу браузъри, които не го правят, можете условно да тествате, както следва:

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();
    }
});

Ако сте на IE6, успех.

person ggozad    schedule 14.02.2012
comment
здрасти Това изглежда добре, но не виждам как това предотвратява извикването на действителната логика от вътрешността на router.home(), както беше споменато по-горе - person Industrial; 16.02.2012
comment
Ключът е да спрете историята да променя местоположението ви. Това можете да направите, като шпионирате window.history, проверите дали работи и след това извикате рутера, без да променяте местоположението си. След това в горния пример можете да проверите дали home() е създал вашите изгледи, променил е DOM или каквото и да е друго, което правите там. - person ggozad; 16.02.2012
comment
Ако искате също да влезете в системата за влизане в router.home(), можете, разбира се, чрез: homeSpy = spyOn(router, 'home').andCallFake(...); Но без pushStateSpy никога няма да стигнете до там. - person ggozad; 16.02.2012
comment
о Но какво да кажем за получаването на евентуалните URI аргументи за метод на маршрут с помощта на това решение? - person Industrial; 16.02.2012
comment
можете да проверите за всичко във вашите шпиони, нали? - person ggozad; 16.02.2012
comment
Но наистина ли е подходящ този начин за тестване на рутери? Сега pushStateSpy става отговорен за извикването на подходящия метод на рутера за URI, вместо самия рутер. нали - person Industrial; 17.02.2012
comment
Е, ако НЕ замените pushState, той просто ще задейства маршрута ви (променете URL адреса си, ако желаете). Вашите тестове вече ги няма ;) Така че да, това е правилно, ако искате да проверите дали pushState ще работи правилно. Като алтернатива, както споменах, погледнете Backbone.history.stop(). Единствената цел да бъде там е тестващите да се държат правилно. Ако не се интересувате от наблюдението на window.history, просто го спрете, премахнете pushStateSpy и запазете останалото. - person ggozad; 17.02.2012
comment
От гледна точка на подходящо: това е много често срещан модел в jasmine to andCallFake да се направи точно това. Тествайте дали нещо ще работи и го направете ръчно, като избягвате сложностите. По същество е подигравателно - person ggozad; 19.02.2012
comment
Това работи чудесно в IE9, но не успява в IE8 поради това, че window.history.pushState е недефиниран. Има ли чист начин за тестване на pushState само ако браузърът го поддържа? - person Dave Cadwallader; 31.07.2012
comment
@ggozad: Забравих да благодаря за този последен фрагмент! Това беше огромна помощ при стартирането и стартирането на тестовите ми спецификации за моя проект Backbone.Subroute. - person Dave Cadwallader; 28.08.2012
comment
@ggozad Благодаря за този отговор. Създадох помощна функция с вариант на вашето решение, което също се подиграва с getHash, за да гарантира, че тестовете имат последователно разбиране за това какво хеш е в играта. - person Andy Armstrong; 23.09.2015

Когато тествам опорен рутер, това, което ме интересува е, че предоставените от мен маршрути извикват функциите, които посочвам с правилните аргументи. Много от другите отговори тук всъщност не тестват това.

Ако трябва да тествате функционалността на някои маршрути, можете да тествате тези функции сами.

Ако приемем, че имате прост рутер:

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

Ето как го правя:

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');
  });
});
person Chris Jaynes    schedule 01.03.2014
comment
Точно това търсех. Благодаря за вградените коментари, обясняващи и вашите разсъждения зад това! - person broox; 10.06.2014

Ето какво в крайна сметка използвах аз самият. Направих макетна версия на рутера, като го разширих и замених методите с празен метод, за да попреча на извикването на допълнителна логика при извикване:

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();
    });

});

Имайте предвид, че sinon.js се използва по-горе за създаване на шпионка, заедно с underscore.js за осигуряване на функцията size.

person Industrial    schedule 26.02.2012
comment
Знам, че това е по-стар въпрос, но е високо в резултатите на Google и мисля, че хората могат да се справят по-добре. Вижте отговора ми по-долу. - person Chris Jaynes; 01.03.2014

Има много добър урок за тестване на гръбнак:

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

person Michal    schedule 13.02.2012
comment
Благодаря. Вече видях това, но няма да ми помогне да заобиколя факта, че моите рутери извикват пренасочвания и така нататък... - person Industrial; 13.02.2012

Трябва да се подиграете на Backbone.Router.route, което е функцията, която се използва вътрешно за свързване на функциите към Backbone.History.

Това е оригиналната функция:

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));
}

можете да направите нещо подобно, което просто извиква функциите, когато рутерът се инициализира:

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

Можете също така да запазите обратните извиквания в обект и с маршрута като име и да извиквате същите стъпки по стъпка:

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

for(i in map){
    map[i]();
}
person Andreas Köberle    schedule 09.02.2012
comment
Здравей Андреас. Все още не мога да го накарам да работи с вашия код - логиката в метода на рутера все още не е спряна - person Industrial; 13.02.2012

Започнах да използвам решението на ggozad за шпиониране на _updateHash, което частично ми помогна. Открих обаче, че тестовете ми са объркани, тъй като хешът никога не се актуализира, така че кодът, който разчита на извиквания към getHash или getFragment, се проваля.

Това, което завърших, е следната помощна функция, която шпионира както _updateHash, така и getHash. Първият записва заявката за актуализиране на хеша, а вторият връща последния хеш, който е бил предаден на _updateHash. Извиквам тази помощна функция в моите тестове, преди да стартирам Backbone хронологията.

    /**
     * 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;
        });
    };
person Andy Armstrong    schedule 22.09.2015