Как стать автором
Обновить

Комментарии 17

Не каждое приложение нуждается в таких вещах как require.JS
Возможность, с одной стороны, не засорять глобальный скоуп, а с другой — иметь простой доступ к содержимому модуля для тестирования была для меня одной из причин перейти на require.js.

Проблема в том, что доступ к функции privateMethod извне уже никак не получить
Есть мнение, что тестировать приватные методы и не нужно. Обоснование — в приватных методах содержатся детали реализации, которые, по определению, юнит-тестами покрывать не надо.

в jasmine удобно пользоваться такой штукой как spyOn
например
it("...", function() {
    var result, fake_data = {....};
    spyOn(someObj, "someFn").andReturn("fake data one");
    result = someObj.fnThatUsesSomeFn(fake_data);
    expect(result).toBe(.....);

    spyOn(someObj, "__someFn").andReturn("fake data two");
    result = someObj.fnThatUsesSomeFn(fake_data);
    expect(result).toBe(.....);
});


Саму же функцию __someFn — мы протестируем отдельно.

Если же к someFn у нас не будет доступа, то возможно придется городить какие-то более хитрые данные для тестирования fnThatUsesSomeFn. Т.е приходится в той или иной степени подгонять данные для нужного ответа от __someFn, что усложняет сам тест
C jasmine не знаком, из беглого чтения доки понял только, что spyOn декорирует методы для более удобного наблюдения за входными и выходными параметрами. Честно говоря, не понял, к чему относится ваш комментарий.
Есть мнение, что тестировать приватные методы и не нужно. Обоснование — в приватных методах содержатся детали реализации, которые, по определению, юнит-тестами покрывать не надо.

я к тому что может их и не нужно тестировать. Но иногда все же хотелось бы иметь к ним доступ из тестового окружения. Например, для случая описанного выше.
Если речь идет о юнит-тестах (а тем более, в случае BDD), то тестируется интерфейс, API. Приватные методы к интерфейсу не относятся, на то они и приватные.
ну допустим
function ShapeManager() {
    var __getSpecificShapeParam = function(shape) {
        ...// тут какие-то хитрые вычисления, которые зависят от фазы луны и магнитных бурь на солнце.....
    }
    return {
        someMethod1: function(shape) {
            var specific_param = __getSpecificShapeParam(shape);
            if (specific_param .....) {
                return true;
            } else {
                return false;       
            }
        },

        someMethod2: function(shape) {
            var specific_param = _getSpecificShapeParam(shape);
            .....
        }
    }
}

В этом примере когда мы будем тестировать someMethod1 и someMethod2 нам придется конструировать какие-то хитрые данные. только для того чтобы внутренняя функция __getSpecificShapeParam — вернула нужное нам значение. Хотя по сути, например в someMethod1 нам нужно протестировать вот этот момент
            if (specific_param .....) {
                return true;
            } else {
                return false;       
            }

И было бы удобно получить доступ к __getSpecificShapeParam и сделать вид что функция возвращает нужное нам значение.
Все, теперь я вас понял.

Я бы в данном случае сделал некий рефакторинг, разнес бы ветвление от параметра и реальную работу в разные методы, а __getSpecificShapeParam пометил бы как protected, а не private.
Кажется логичным ответ на вопрос, что надо ставить обработчики данных в одну кучу, обработчики событий в другую. Так я отказываюсь тестировать ту часть которая красит инпут в нужный цвет, а концентрируюсь на том, чтобы сделать возможным протестировать ту часть программы, где вычисляется в какой цвет красить инпут.
Это называется разделение бизнес-логики и логики представления. И, скажем, разработчики AngularJS (как наиболее «тестоориентированного» MV*-фреймворка) уже давно позаботились о том, чтобы такие вещи писались и тестировались одним движением левой пятки.
Может кто подскажет как тестировать API, например window.location = 'http://habrahabr.ru', вызовы history API и тд. Также как правильно тестировать изменение DOM, тк разные браузеры по разному могут генерить html. И как тестировать jQuery объекты, когда $('.some-class') !== $('.some_class')?
1. В случае с API браузера, лучше написать метод, который будет выполнять необходимые вам действия:

namespace.util = {
    redirect: function(url) {
         window.location = url;
    }
}

а в тестах подменить этот метод, например с помощью spy в sinon следющим образом:

sinon.spy(utils, "redirect");
//...
expect(namespace.util.redirect).have.been.calledOnce;
namespace.util.redirect.restore();

2. По поводу DOM, он у вас должен генерироваться одинаково во всех браузерах, если сгенерированный HTML разный, тесты должны падать, или вы имеете в виду что то другое?

3. jQuery объекты можно сравнивать с помощью deep equal, в chai это делается так:

expect($('h1')).to.be.deep.equal($('h1'));

Больше методов для тестирования jQuery в chai-jquery.
По поводу DOM я сталкивался с разной очередностью тегов (например jsfiddle.net/qM6AD/2/ сравни ff и chrome), лишними пробелами в style. Семантически получается одно и то же, но строгое равенство html не выполняется. Хотя возможно это связано с работой jQuery и phantomJS.

Вроде даже в каких-то браузерах появлялись специфические аттрибуты, но сейчас не могу найти.
*разной очередностью атрибутов
Тогда могу только посоветовать сравнивать не html как есть, а какие то свойства/атрибуты в HTML, которые для вас критичны, в общем chai-jquery этим и занимается.
Если оформить валидатор объектом, то тестировать станет намного проще. Вот, смотрите:

function Validator(regexp, ajax) {
    this.regexp = regexp;
    this.ajax = ajax;
}
Validator.prototype.validate = function(str, callback) {
    if(/*validate by regexp*/) {
        ajax({/*validate on server*/}).success(function(data) {
            callback(data)
        });
    }
    else {
        callback(false);
    }
}


Использование валидатора в приложении:

var validator = new Validator(/\d*/, $.ajax);
$('input').on('keydown', validator.validate.bind(validator));


Как уже выше заметили, в jasmine есть удобная функция шпионов. Создадим шпиона и подставим его вместо $.ajax (для этого возможность конфига и оставили). Тогда в тесте можно будет до всего добраться, а сам он получится коротким и ясным

var ajaxMock, validator;
beforeEach(function() {
    ajaxMock = jasmine.createSpy('ajax').andReturn({
        success: jasmine.createSpy('ajaxSuccess')
    });
    validator = new Validator(/\d*/, ajaxMock);
});
it('a test', function() {
    var callbackSpy = jasmine.createSpy('callback')
    validator.validate('123a', callbackSpy);
    expect(callback).not.toHaveBeenCalled();
    expect(ajaxMock).toHaveBeenCalled();
    //вот как-то так можно добраться до скрытой внутри лямбды
    ajaxMock.success.mostRecentCall.args[0](true);
    //а потом проверить, что она отработала правильно
    expect(callbackSpy).toHaveBeenCalledWith(true);
});

P.S. Можно было не выносить ajax в конфиг и написать просто ajaxMock = spyOn($, 'ajax'), но тогда получается та самая нечистая функция, поэтому лучше так не делать
Ни слова не сказано про mock-объекты, а именно их использование в юнит-тестах требует писать качественный код. Умение, например, разнести в разные части кода работу с DOM, DOMEvents и логикой модуля, — имхо, золотое умение. И не покрывать тестами и так покрытый тестами jQuery, а тестировать логику работы модуля, а не его отображения.

var MyExample = function (options, view) {
    var _view = view,
        _displayed = false;

    this.show = function () {
        if (!_displayed) {
            _displayed = true;
            _view.show();
        }
    };

    this.hide = function () {
        if (_displayed) {
            _displayed = false;
            _view.hide();
        }
    };
};


В таком коде нам совершенно наплевать, каким образом реализуется работа с DOM при попытке показать модуль — может там просто documentgetElementById("MyId").style.display = true, а может $("#MyId").fadeIn(2000);.
Впрочем, не уверен, что jasmine поддерживает полноценные мок-объекты по прототипу. Все-таки в spy есть одно ограничение — приходится писать название методов в строки. Моя EDI не поддерживает смартинпут в кавычках. Может, кто-то объяснит, как создавать моки в Jasmine без перебивания руками всех нужных методов?
Если я правильно понял вопрос, то вам поможет такой код

function isFunction(object) {
 return object && getClass.call(object) == '[object Function]';
}
function mockObject(obj) {
     for(var key in obj)
          if(isFunction(obj[key])) {
               spyOn(obj, key)
          }
     }
}
Ну круто, когда это все в коробке есть, а не писать свой велосипед для адекватных мок-объектов? Это все-таки связано с тем, что Jasmine, как и QUnit они себя позиционируют как именно движки для тестирования работы JS в браузере. Ну мой внутренний Роберт Мартин кричит, что если какая-то часть кода может по-разному работать в разных средах, то ее надо вынести в абстракцию и покрыть комментариями. И потом не придется париться, запуская тесты в разных браузерах. Но это чисто мои тараканы.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории