Глубинное погружение в test-driven JavaScript

    Многие JavaScript-фреймворки предлагают свое представление о том, как должен выглядеть код. Более того, речь идет не просто о стиле, речь идет о способе написания сценариев. Это обусловлено практически абсолютной демократичностью JavaScript, да-да, именно таким является мультипарадигменный язык с С-подобным синтаксисом, прототипным наследованием, динамической типизацией и реализацией разнящейся от браузера к браузеру. Поэтому, когда речь идет о test-driven JavaScript я понимаю, что речь идет не просто об особом стиле программирования, но об особых технических принципах для особого фреймворка позволяющего тестировать JS приложения.

    В этой статье я буду спорить сам с собой что же из себя представляет тестопригодный JavaScript-код и в какую цену он обойдется, ежели его начать использовать.

    Внимание: длиннопост.

    Далее я буду предполагать, что читатель немного знаком с qUnit и/или Jasmine. Слегка пройтись по верхушкам можно за 10 минут, для этой статьи этого достаточно. Как заведено, на философию и обобщения уходит куда больше времени и энергии. На самом деле, для себя я ответил на все вопросы поставленные в аннотации к этой статье стоило только прочитать это и вспомнить что вообще такое юнит-тесты. Дело в том, что никогда не приходилось заниматься юнит-тестами front-end'a, поэтому я скептически отнесся к такой технологии. Постараюсь ниже изложить основные затруднения. Прошу также не воспринимать статью как манифест за или против юнит-тестирования клиентского кода.

    Кроме того, надо помнить, что фреймворки для тестирования отличаются друг от друга. Я постарался избавиться от главных разногласий, но несомненно, что, например, Jasmine дает большую свободу в написании тестопригодного кода, чем qUnit. Тем не менее, описанные в статье затруднения возникают и там, и там.

    И так поехали.

    Анонимные функции


    Первый вопрос был: «Ок, а как тестировать анонимные функции? Что мне делать если мой код сплошные анонимные коллбэки и прочая лямбда-магия?». Официальная документация вполне доступна объяснила что же делать с асинхронными вызовами, но ни одна их них не была анонимна.

    var callBack = function(x){	... }
    someAsyncFunction(someOptions, callBack);
    
    и
    someAsyncFunction(someOptions, function(x){ ... }); 
    

    Несколько разные вещи, согласитесь. А ведь JavaScript приложения полнятся такими конструкциями. Традиционная для jQuery конструкция

    $(document).ready(function(){
    	// zomg teh lambda application
    })
    

    ставящее перед собой цель не допустить выполнение приложения до того, как не будет готов DOM. Или даже вполне себе обычное оборачивание приложения в анонимную функцию, чтобы не захламлять глобальное пространство имен:

    (function(){ ... })()
    

    Не очень понятно, как тестировать эти функции, если к ним нет доступа по идентификатору. Если вся их суть заключается в том, чтобы к ним не было доступа. Выходит к ним нужно предоставить доступ.

    $(document).ready(Application)
    
    function Application () {
    	// application
    }
    

    И теперь приложение можно тестировать в qUnit так:

    test('Application Constructor is loaded', function () {
      ok(window.Application !== undefined , 'Приложение найдено');  
    });
    

    Но есть одна проблема. А что если я не хочу засорять глобальное пространство имен? К сожалению хотя-бы на одно имя придется раскошелиться. Как минимум есть такая практика как Backdoor Manipulation. Нечто похожее в JS можно реализовать через глобальный объект window, в которой можно создать свойство в котором и хранить ссылку на приложение, например:

    (function(){
    	function Application(){
    		//вот тут у нас будет приложение
    	}
    
    	window.testBackdoor = { Application : new Application() }; // или просто Application(), ежели это не конструктор
    })(window)
    
    //А тестировать это приложение мы будем вот так.
    var app = window.testBackdoor.Application;
    
    test('Application Constructor is loaded', function () {
      ok(app !== undefined , 'Приложение загрузилося');  
    });
    

    Вот так. Но мы вводим лишнюю конструкцию и это все равно не избавляет нас от лишних имен в объекте window. Это печально. На этой стадии мой внутренний спорщик задает резонный вопрос: собственно, а почему вы тестируете приложение целиком, где же юнит-тесты? Справедливо. Я тестирую приложение целиком. Но ведь именно таковы приложения на JS чаще всего, они (почти) всегда суть одна целая. Никто не станет выделять логику приложения от той части, которая обрабатывает данные, подключать эти модули разными путями, если это и правда не гигантское приложение, которому нужен именно такой подход. Не каждое приложение нуждается в таких вещах как require.JS.

    Аргумент #1: Надо делить приложение на разные кусочки-модельки и тестировать их по-отдельности, ведь в этом смысл юнит-тестов. В каждом юните можно иметь свое пространство имен (реализовав весь юнит в отдельной функции) и проблема решается.

    Контраргумент #1: JavaScript не ООП язык в классическом понимании, чтобы была возможность тестировать методы класса. Реализация ООП, предоставляющего возможность тестировать отдельные объекты в JavaScipt чревата проблемами чрезмерного кода. (Утверждение, коему я не приведу строгих доказательств, лишь напомню, что в JS отсутствуют public и private ключи. К тому же достаточно вспомнить то во что превращаются милые сердцу классы и их наследования в CoffeeScript при трансляции в JavaScript).

    Но раз существует test-driven JavaScript, то существует такой стиль организовать объектную модель приложения так, чтобы ее можно было бы протестировать…

    Наследование



    Вообще, ООП, прототипное наследование и прочее в JavaScript — это благодатная почва для жаркой полемики, коей не место в этой статье. Но перед тем, как начать исследовать наследование в test-driven контексте, рекомендую почитать Дугласа Крокфорда и Николаса Закаса в «Оптимизаци JavaScript приложений», чтобы быть в курсе положительных и отрицательных сторон тех или иных способов организации наследования в JavaScript.

    Тестопригодное наследование пожалуй одна из тех частей, которые приятно удивили меня тем, что не вызвали лишних затруднений, когда я придерживался канонического прототипного наследования на JavaScript. Действительно, если у нас есть доступ к объекту, то мы тотчас же можем получить доступ к его методам и немедленно протестировать их вдоль и поперек. Например я добавляю в прототип какого-то объекта функцию вычисления абсолютного значения числа:

    myObject.prototype.abs = function(x){return x>0 ? x : -x;} 
    //теперь могу тестировать метод обратившись к нему как myObject.abs
    

    Вся эта простота выходит за счет одной детали, которую многие считают значительным недостатком JavaScript'a. Это то, что у объекта нет private и public свойств. Они все доступны. Таким образом мы опять приходим к тому, что тестировать можно только те функции и методы, к которым осуществлен доступ. Стоит только начать имитировать публичные и приватные свойства объекта, например так:

    function myClass (options){
    	var privateMethod = function(){ ... }
    
    	var that = {
    		publicMethod : function(){ ... }
    	}
    
    	return that;
    }
    

    Доступ к замыканию publicMethod осуществим, так как возвращается объект, который содержит его в качестве свойства. Проблема в том, что доступ к функции privateMethod извне уже никак не получить, хоть она и занимает в scope функции myClass место, и внутри всех замыканий myClass к нему можно обратиться, но ведь для любых других других scope уровнем выше доступа к функции нет. Возвращаемся к параграфу 1.
    Это особо значимо, когда мы пишем приложения где используется традиционный ООП-шаблон, в котором несколько объектов со множеством методов, внутренней логики и прочего, где нет factory-метода плодящего много экземпляров, и их наследованных экземпляров. Короче говоря…

    Аргумент #2: Если использовать традиционное для JavaScript прототипное наследование, то вся объектная модель приложения автоматически доступна для тестов без лишних усилий.

    Контраргумент #2: Есть тысяча и один способов реализовать наследование (многие из которых весьма популярны) в JavaScript, которые лишают разработчика доступа к методам экземпляра класса.

    Проблема реализации наследования и объектного программирования в test-driven JavaScript в том, что одни методы влияют на состояние экземпляра в целом. Как же тестировать, что при определенном событии экземпляр изменил какое-то внутреннее свойство, которое не зависит от других функций, например, фигурка при наведении курсора сменила свой цвет? При условии, что мы доверяем тем фреймворкам с помощью которых реализуем это отображение. А ведь очень многое, что мы делаем с помощью разных фреймворков меняет состояния и только состояния.

    К сожалению, идеи юнит-тестирования в одном расходятся с реалиями. Никто (почти никто) не разрабатывает модель, потом пишет JavaScript — сценарий, а потом только делает под это дело GUI, чаще всего все происходит совсем наоборот, программист получает интерфейс приложения и слышит «сделай так, чтобы работало». Я не говорю, что первый случай невозможен или избыточен, напротив — это лучший путь, но увы, front-end — это то, что воспринимается как этап разработки, служащий для отображения работы back-end'а.

    Возвращаясь к теме состояний системы, я прихожу к главной проблеме. Предположим, что мы абсолютно доверяем всяким jQuery и уверены, что функция получившая в аргумент строку вида "#000000", будет красить фигурку именно в этот цвет и сделает это без ошибок. Предположим, что фреймворки для работы с DOM, и занимающиеся прочим отображением не столь важно тестировать, как функции, где происходит обработка данных в приложении. Но как отделить одно от другого? Как написать чистые функции, которые и имеет смысл тестировать, не вписывая туда побочные действия?

    var a = 10;
    function f(x) {
    	a = x;
    }
    

    Эта функция не чиста, увы. Она принимает данные и меняет состояние системы (значение переменной a).

    Чистые функции


    Чистые функции — это хорошо. Хорошо, когда функции не меняют состояния системы. Когда функции получают данные, что-то с ними делают и возвращают данные. И больше ничего не делают.
    Но в JavaScript есть два нюанса.
    Первый: В JavaScript функции — это объекты, а системы — это в общем-то тоже объекты, то есть состояния систем конструируются функциями. Состояния систем — это состояния функций. Нет четкого разделения понятия объект и метод. Все в конечном счете сливается в дзен объектов или функций, если угодно. Иначе говоря, функциями мы конструируем объекты, функциями мы вычисляем данные и передаем их в функции, которые меняют изначальные объекты. (здесь должна быть картинка с Xzibitом и подписью 'Yo dawg')
    Второй: Разве суть клиентского JavaScripta не в сайд-эффектах? То есть в отображении, в динамизации. Когда вы в последний раз брали какой-нибудь зверский интеграл Римана чтобы результирующие данные вернуть пользователю в консоль браузера? Я сильно удивлюсь, если кому-то хоть раз приходилось решать такие задачи на стороне клиента, да еще и без отображения. Если в приложении нет сколько-нибудь важной логики, а лишь одно отображение, то по-сути и тестировать здесь нечего.

    Вот довольно хрестоматийный пример:
    Дан инпут, надо по событию keydown инициировать валидацию содержимого инпута по регулярному выражению. Если введенная строка валидна, то красим инпут в зеленый, иначе в красный.

    $("#myPrettyInput").on("keydown", function(){
    	if(проводим_здесь_валидацию_инпута){
    		... красим инпут в зеленый
    	}else{
    		... красим инпут в красный
    	}
    });
    

    Чтобы отделить обработку данных (валидация) от сайд-эффектов (изменение CSS) можно проделать следующий кунштюк:

    $("#myPrettyInput").on("keydown", function(){
    	var val = $(this).val();
    	var regex = ... какое-нибудь регулярное выражение;
    	if(validate(val, regex){
    		// красим инпут в зеленый
    	}else{
    		// красим инпут в красный
    	}
    });
    
    var validate = function(str, rgx){
            // кристально чистой души функция. Возвращает true или false 	
    };
    

    С одной стороны, а что плохого? Подумаешь лишнюю функцию написали? Ведь в первом случае, скорее всего в if стояло бы какое-нибудь уродливое выражение типа $(this).val().match(regex).length, а теперь у нас вполне себе опрятная функция. И неважно, что в теле функции то же самое выражение, главное ее можно тестировать! Действительно, на таком упрощенном примере все выглядит вполне оправдано и прилично. Кажется, что мы добились тестопригодности выделением одной лишь функции, но…

    Аргумент #3: Мы получили тестопригодный код и в любой момент можем проверить, работает ли наша валидация или нет написав на нее сколь угодно много автоматизированных тестов. Кроме того, мы избавились от гигантских выражений в обработчике событий.

    Контраргумент #3: Мы ввели еще одну переменную. А что если валидация не столь проста, а включает в себя ajax вызов? Например, проверка не занят ли юзернейм. И чтобы реализовать разделение сайд-эффектов от строгой обработки данных нужно нечто большее, чем вывести функцию из обработчика событий.

    Контрпример:
    var validate = function(str, regex){
    	if(проводим_здесь_валидацию_инпута_по_regexp){
    		$.ajax({
    			...
    			succsess : function(data){
    				// лямбда-функция, да еще и обладающая побочными эффектами 
    				// здесь происходит валидация data
    			}
    		})
    	}
    	...
    }
    

    К сожалению, одной идентификацией коллбэка не обойтись.

    var validate = function(str, regex){
    	function ajax_validate(data){
    		// здесь происходит валидация data
    	}
    
    	if(проводим_здесь_валидацию_инпута_по_regexp){
    		$.ajax({
    			...
    			success : function(data){
    				if(ajax_validate(data)){
    					... какие-то сайд-эффекты
    				}
    			}
    		})
    	}
    	...
    }
    

    Конечно в предыдущем листинге, функция ajax_validate чиста (возвращает результат валидации, а это всегда или true или false), но тем не менее ее никак не протестируешь, потому что — это переменная внутри validate. Сделать ее замыканием validate? Дурацкое решение — от этого validate превращается в класс, перестает быть чистой. Может тогда превратить все строгие проверки, которые возвращают true или false в замыкания какого-нибудь отдельного модуля, который будет занят исключительно всякими валидациями в приложении? С точки зрения тестопригодности — это вполне удовлетворительное решение, с точки зрения модульности тоже. Но надеюсь вы проследили как увеличился объем работы. И я ведь по-сути ничего особого в приложение не добавил. Я спрашиваю себя, где золотая середина?

    Кажется логичным ответ на вопрос, что надо ставить обработчики данных в одну кучу, обработчики событий в другую. Так я отказываюсь тестировать ту часть которая красит инпут в нужный цвет, а концентрируюсь на том, чтобы сделать возможным протестировать ту часть программы, где вычисляется в какой цвет красить инпут. Но опять же, с оговоркой, что если приложение не слишком большое/сложное/запутанное. Если уж и правда оно таково, то лучше да, ввести лишний модуль, потратить время, но добиться тестопригодности и удобства дальнейшего использования. А ежели приложение не требует особых ухищрений, я бы поступил так:

    $("#myPrettyInput").on("keydown", function(){
    	var val = $(this).val();
    
    	if(validate(val,regex)){
    		var ajaxValidationResult = false;
    
    		$.ajax({
    			...
    			success : function(data){
    				if(ajax_validate(data)){
    					ajaxValidationResult = true; //вот вам и сайд-эффект
    				}
    			}
    			...
    		});
    		
    		setTimeout(function(){
    			if(ajaxValidationResult){
    				// красим инпут в зеленый
    			}else{
    				// красим инпут в красный или не красим вовсе, кому как хочется
    				setTimeout(arguments.callee, 75);
    			}
    		})		
    	}else{
    		//красим инпут в красный
    	}
    });
    
    //а вот тут я бережно храню свои чистые функции
    var validate = function(str, regex){ ... }
    var ajax_validate = function(data){ ... }
    

    Для меня это вполне приемлемое отношение лишней работы к пользе от тестопригодности. Но, золотой серединой я это назвать не могу. Какие мои решения? На самом деле никаких универсальных. Внутрь JavaScript встроено много возможностей делать его настолько тестопригодным, насколько вы того пожелаете. Здесь всплывает функциональная природа JavaScript'a. Где можно передавать функции в функции и возвращать тоже функции. Это удобно в том смысле, что логику приложения можно выполнить в довольно свободном декларативном стиле (это так часть, которая не имеет отношения к юнит-тестам), в то время как те части программы, которые отвечают за обработку данных оставить тестопригодными. Я же чаще всего пользуюсь следующей схемой:

    function App(){
    
    	function Vehicle(){
    		//здесь я храню не тестируемые переменные и методы
    		var somePrivateMethod = function(){ ... }
    
    		var that = {
    			//здесь я храню тестируемые функции и переменные
    			somePublicMethod : function(){ ... } 
    		}
    
    		return that;
    	}	
    
    	function Car(){
    		//так я реализую наследование и взаимосвязь между объектами
    		var that = Vehicle();
    		//так я расширяю методы класса
    		var that.anotherPublicMethod = function(){ ... }
    
    		return that;
    	}
    
    	function IndependentModule(){ //независимый модуль, который не используется в других классах
    		vat that = {
    			independentFunction : function() { ... }
    		}
    		return that;
    	}
    
    	function Factory(){
    		var that = {
    			car : Car(),
    			bicycle : Vehicle(),
    		}
    		return that;
    	}
    
    	window.testBackdoor =  { Factory : Factory(), Module : IndependentModule() };
            //или
    	return { Factory : Factory(), Module : IndependentModule() };
    }
    


    Таким образом можно получить доступ к замыканиям любого класса. В конце же я могу выбрать способ получения доступа либо через бэкдор либо просто возвращая нужный объект. Однако, такая имитация public/private методов в целом не выполняет саму идею public/private методов, а служит лишь разграничителем между функциями доступными из других scope или нет, что является довольно уродливой надстройкой для того, чтобы обеспечить тестопригодность, а никак не для разделения public и private методов классического ООП. В большей степени таким образом имеет смысл разделять чистые функции и функции с побочными эффектами. К тому же еще надо помнить, что такая организация структуры приложения далеко не самая производительная и больше подходит для больших многосвязных приложений с несколькими модулями, чем для приложений, где используются множественные экземпляры классов.

    Возвращаясь к разделению обработки данных и отображения, скажу что некоторые библиотеки принуждают к разделению чистых функций от сайд-эффектов (в основном это функциональные и/или декларативные). Другие же к сожалению не особо поддерживают эту идею. Хотя за то время, что я знаком с JavaScript я убежден, что нет ничего невозможного. Вопрос совсем в другом. Ценой каких усилий?
    Например, в этой статье, я тестирую довольно простое приложение (игрушку) и потому как я использую bacon.js — декларативную библиотеку, суть которой избавить разработчика от ада вложенных коллбэков, повешенных на слушатели событий, то я в целом смог без лишних потерь привести приложение к тестопригодному виду.

    Резюме:
    К сожалению, ничего более вразумительного, чем довольно банальное «каждый инструмент уместен в своем месте» я не могу сказать. Я лишь несколько раз использовал qUnit и Jasmine в тестировании клиентского кода для более или менее крупных приложений. Но это были те случаи, когда я еще не написав ни строчки кода уже предчувствовал, как именно в этом приложении я смогу разделить сайд-эффекты от обработки данных, и что если я буду писать тестопригодный код, то приложение не превратиться в невразумительный фарш.

    Напоследок, хотел бы сказать, что сама идея юнит-тестирования хорошо сходится с модульной сущностью NodeJS. Пихать в эту статью еще и test-driven программирование на платформе NodeJS было бы лишним, хотя тема определенно волнующая. Поэтому если кому-нибудь будет интересно, я напишу об этом в следующей статье.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 17

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

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

        +2
        в 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, что усложняет сам тест
          +1
          C jasmine не знаком, из беглого чтения доки понял только, что spyOn декорирует методы для более удобного наблюдения за входными и выходными параметрами. Честно говоря, не понял, к чему относится ваш комментарий.
            +1
            Есть мнение, что тестировать приватные методы и не нужно. Обоснование — в приватных методах содержатся детали реализации, которые, по определению, юнит-тестами покрывать не надо.

            я к тому что может их и не нужно тестировать. Но иногда все же хотелось бы иметь к ним доступ из тестового окружения. Например, для случая описанного выше.
              +1
              Если речь идет о юнит-тестах (а тем более, в случае BDD), то тестируется интерфейс, API. Приватные методы к интерфейсу не относятся, на то они и приватные.
                +1
                ну допустим
                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 и сделать вид что функция возвращает нужное нам значение.
                  0
                  Все, теперь я вас понял.

                  Я бы в данном случае сделал некий рефакторинг, разнес бы ветвление от параметра и реальную работу в разные методы, а __getSpecificShapeParam пометил бы как protected, а не private.
        +1
        Кажется логичным ответ на вопрос, что надо ставить обработчики данных в одну кучу, обработчики событий в другую. Так я отказываюсь тестировать ту часть которая красит инпут в нужный цвет, а концентрируюсь на том, чтобы сделать возможным протестировать ту часть программы, где вычисляется в какой цвет красить инпут.
        Это называется разделение бизнес-логики и логики представления. И, скажем, разработчики AngularJS (как наиболее «тестоориентированного» MV*-фреймворка) уже давно позаботились о том, чтобы такие вещи писались и тестировались одним движением левой пятки.
          0
          Может кто подскажет как тестировать API, например window.location = 'http://habrahabr.ru', вызовы history API и тд. Также как правильно тестировать изменение DOM, тк разные браузеры по разному могут генерить html. И как тестировать jQuery объекты, когда $('.some-class') !== $('.some_class')?
            +1
            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.
              0
              По поводу DOM я сталкивался с разной очередностью тегов (например jsfiddle.net/qM6AD/2/ сравни ff и chrome), лишними пробелами в style. Семантически получается одно и то же, но строгое равенство html не выполняется. Хотя возможно это связано с работой jQuery и phantomJS.

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

              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'), но тогда получается та самая нечистая функция, поэтому лучше так не делать
                +1
                Ни слова не сказано про 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 без перебивания руками всех нужных методов?
                  0
                  Если я правильно понял вопрос, то вам поможет такой код

                  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)
                            }
                       }
                  }
                  
                    0
                    Ну круто, когда это все в коробке есть, а не писать свой велосипед для адекватных мок-объектов? Это все-таки связано с тем, что Jasmine, как и QUnit они себя позиционируют как именно движки для тестирования работы JS в браузере. Ну мой внутренний Роберт Мартин кричит, что если какая-то часть кода может по-разному работать в разных средах, то ее надо вынести в абстракцию и покрыть комментариями. И потом не придется париться, запуская тесты в разных браузерах. Но это чисто мои тараканы.

                Only users with full accounts can post comments. Log in, please.