Правильно ли тестируете маршрутизаторы в backbone.js?

Итак, я только начал писать тесты для моего незавершенного приложения javascript, используя sinon.js и jasmine.js. В целом работает довольно хорошо, но мне также нужно иметь возможность протестировать свои маршрутизаторы.

Маршрутизаторы в их текущем состоянии будут запускать ряд просмотров и другие вещи, завершая текущий jasmine.js тест, вызывая Backbone.navigate в зависимости от состояния приложения и его действия пользовательского интерфейса.

Итак, как я могу проверить, что маршрутизация в разные места будет работать, сохраняя при этом маршрутизаторы в «песочнице» и не позволяя им менять маршрут?

Могу ли я настроить какую-то фиктивную функцию, которая будет отслеживать изменения pushState или что-то подобное?


person Industrial    schedule 09.02.2012    source источник
comment
Баунти включена! Жду еще идей по этому поводу   -  person Industrial    schedule 13.02.2012


Ответы (6)


Вот низкоуровневый способ сделать это с помощью jasmine, проверяя, что pushState работает должным образом и что ваш маршрутизатор настраивает все правильно... Я предполагаю, что router был инициализирован и имеет сопоставленный маршрут home к ''. Вы можете адаптировать это для других ваших маршрутов. Я также предполагаю, что вы выполнили инициализацию своего приложения 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 для 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