Тестирование маршрутизаторов в backbone.js правильно?
Итак, я только начал писать тесты для моего исполняемого javascript-приложения, используя sinon.js
и jasmine.js
. Работает довольно хорошо в целом, но мне также нужно проверить свои маршрутизаторы.
Маршрутизаторы в своем текущем состоянии будут вызывать несколько видов и других вещей, завершая текущий тест jasmine.js
, вызывая Backbone.navigate
в зависимости от состояния приложения и UI itneraction.
Итак, как я могу проверить, будет ли работать маршрутизация в разных местах, сохраняя маршрутизаторы "изолированными" и не позволяя им изменять маршрут?
Могу ли я настроить какую-то макетную функцию, которая будет отслеживать изменения pushState или аналогичные?
Ответы
Ответ 1
Вот что я в конечном итоге использовал сам. Я сделал макет версии маршрутизатора, расширив его и переопределив методы с помощью пустого метода, чтобы он не вызывал любую последующую логику при вызове:
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
.
Ответ 2
Здесь низкоуровневый способ сделать это с жасмином, тестируя, что pushState работает так, как ожидалось, и что ваш маршрутизатор правильно настраивает...
Я предполагаю, что router
был инициализирован и имеет домашний маршрут, сопоставленный с ''. Вы можете адаптировать это для своих других маршрутов. Я также предполагаю, что вы сделали в своей инициализации приложения 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();
...
});
});
Вы можете эффективно добиться аналогичных действий, сделав 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, удачи.
Ответ 3
Когда я тестирую магистральный маршрутизатор, меня волнует то, что указанные мной маршруты ссылаются на функции, которые я задаю с помощью правильных аргументов. Многие другие ответы здесь не проверяют это.
Если вам нужно проверить функциональность некоторых маршрутов, вы можете сами проверить эти функции.
Предполагая, что у вас простой маршрутизатор:
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');
});
});
Ответ 4
Существует очень хороший учебник по тестированию основы:
http://tinnedfruit.com/2011/04/26/testing-backbone-apps-with-jasmine-sinon-3.html
Ответ 5
Вы должны mock 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]();
}
Ответ 6
Я начал использовать ggozad решение шпионажа на _updateHash
, которое частично сработало для меня. Тем не менее, я обнаружил, что мои тесты были сбиты с толку, потому что хеш никогда не обновлялся, поэтому код, который полагался на вызовы getHash
или getFragment
, терпел неудачу.
В результате я получил следующую вспомогательную функцию, которая шпионит на _updateHash
и getHash
. Первый записывает запрос на обновление хэша, а последний возвращает последний хеш, который был передан в _updateHash
. Я вызываю эту вспомогательную функцию в своих тестах, прежде чем я начну историю с базой.
/**
* Prevent Backbone tests from changing the browser URL.
*
* This function modifies Backbone so that tests can navigate
* without modifying the browser URL. It works be adding
* stub versions of Backbone hash functions so that updating
* the hash doesn't change the URL but instead updates a
* local object. The router 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;
});
};