Разработка jQuery, управляемая тестами

Элайджа Мэйнор (Elijah Manor) | 25 января 2010 г.

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

Многие из вас уже могут быть знакомы с модульным тестированием в других языках, таких как .NET, Ruby и Java, но в мире JavaScript такая практика используется нечасто. Так как большая часть клиентского кода помещается в браузер для расширения возможностей пользовательского интерфейса и улучшения взаимодействия с пользователями, использование модульных тестов, охватывающих эти области разработки, также имеет смысл.

Разработка, управляемая тестами

Разработка, управляемая тестами (Test-driven development, TDD) — это процесс, в котором тесты пишутся еще до фактического написания кода приложения. Джеффри Палермо (Jeffery Palermo) написал хорошую вводную статью для MSDN, Рекомендации по разработке, управляемой тестами. В этой статье он очерчивает основы процесса TDD. Здесь я подытожил шаги из его материала, но я призываю прочесть его статью полностью.

  1. Разберитесь в области применения компонента и требованиях к нему
  2. Создайте тест, который заведомо закончится неудачей
    • Вызовите метод, который не существует или пока работает неправильно
    • Это важно, так как нежелательно, чтобы тесты были выполнены случайно
  3. Добейтесь прохождения сбойного теста, добавляя минимальное количество кода
    • Измените код так, чтобы он проходил тест, но не переборщите
    • Написание минимального кода помогает не писать код, который не нужен
  4. Измените свой код, чтобы внести все необходимые улучшения
    • Именно в этот момент можно переработать методы, переименовать переменные, изменить дизайн интерфейсов и т. д.

Средства JavaScript Trade

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

Средство модульного тестирования

Так как большая часть JavaScript, с которым я работаю, связана с jQuery, мне имеет смысл выбрать в качестве своего средства тестирования QUnit. Программа QUnit была разработана Джоном Ризигом (John Resig) для поддержки тестирования как обычного JavaScript, так и jQuery.

Средство макетирования

Я проанализировал множество сред макетирования и после длительного изучения обнаружил проект QMock, который отвечал большинству моих требований к платформе макетирования и создания заглушек. Я обнаружил, что большинство платформ макетирования либо не поддерживает создание заглушек, либо, если они поддерживают создание заглушек, они не поддерживают методы обратных вызовов, часто используемых в jQuery. Мне пришлось внести некоторые незначительные изменения в основной код для поддержки метода jQuery.ajax. Я планирую применить свой код как исправление для проекта GitHub.

Средство охвата кода

Когда я писал модульные тесты, я начал интересоваться тем, каково покрытие моего тестирования. Другими словами, мне хотелось знать, какая часть моего проекта проверялась тестами. Я обнаружил парочку подходящих средств, но программа JSCoverage лучше соответствовала моим потребностям. В примерах, приводимых ниже в этой статье, я использую этот инструмент.

Пример подключаемого модуля jQuery

В качестве примера для проверки этих концепций я разработал простой подключаемый модуль jQuery, который является альбомом и средством просмотра фотографий для веб-альбомов Google Picasa. Так как Google предоставляет поддержку JSONP в своих интерфейсах API, можно использовать jQuery AJAX для получения данных об альбоме и фотографиях. Вот несколько основных возможностей, которые должен поддерживать подключаемый модуль jQuery:

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

Примечание. Для реализации вкладок и модальных окон подключаемый модуль зависит от библиотеки пользовательского интерфейса jQuery.

На рис. 1 показано, как будет выглядеть подключаемый модуль jQuery после разработки.


Рис. 1. Пример подключаемого модуля jQuery

Создание тестов для наших модулей

Сначала я сфокусируюсь на создании тестов модулей, а затем продолжу разработку из этой точки. Если средство QUnit еще не установлено, можно перейти в Репозиторий jQuery/QUnit GitHub и нажать кнопку "Download Source" (Загрузить источник), чтобы получить Zip- или Tar-файл.

После распаковки файла архива появятся две папки: QUnit и Test. Папка QUnit содержит платформу QUnit и поддерживающие стили CSS, а папка Test содержит средство выполнения тестов index.html и два файла JavaScript. Если открыть файл index.html в браузере, он будет выглядеть примерно так, как показано на рис. 2.


Рис. 2. Файл index.html из QUnit, выполняющий все соответствующие тесты

Теперь откройте файл index.html в текстовом редакторе, удалите существующие скрипты test.js и same.js, заменяя их своими зависимостями, например следующими:

  • библиотека jQuery;
  • библиотека пользовательского интерфейса jQuery;
  • библиотека QMock;
  • подключаемый модуль jQuery веб-просмотра Picasa (пустой файл);
  • тесты веб-просмотра Picasa (пустой файл).

Вот пример разметки:

<!DOCTYPE html>
<html>
<head>
   <title>QUnit Test Suite</title>
   <link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen">
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>   
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js"></script>
   <script type="text/javascript" src="qunit.js"></script>
   <script type="text/javascript" src="qmock.js"></script>
   <script type="text/javascript" src="jquery.picasawebviewer.js"></script>
   <script type="text/javascript" src="jquery.picasawebviewer.tests.js"></script>
</head>
<body>
   <h1 id="qunit-header">QUnit Test Suite</h1>
   <h2 id="qunit-banner"></h2>
   <div id="qunit-testrunner-toolbar"></div>
   <h2 id="qunit-userAgent"></h2>
   <ol id="qunit-tests"></ol>
</body>
</html>

Первый тест модуля, который я добавляю на страницу jQuery.PicasaWebViewer.Tests.js — это тест, проверяющий значения по умолчанию для подключаемого модуля:

module("Picasa Web Viewer");

test("Default Options", function() {
    same($.picasaWebViewer.defaultOptions.urlFormat, 'http://picasaweb.google.com/data/feed/api/user/{0}?alt=json-in-script');
    same($.picasaWebViewer.defaultOptions.albumTitleMaxLength, 15);
    same($.picasaWebViewer.defaultOptions.defaultDialogWidth, 600);
    same($.picasaWebViewer.defaultOptions.defaultDialogHeight, 400);        
});

В настоящий момент подключаемый модуль jQuery с именем PicasaWebViewer отсутствует, как и все публичные методы, которые мы можем вызвать, поэтому при выполнении этих тестов я получу красный результат, означающий, что тесты не выполнены. (См. рис. 3.)


Рис.3. QUnit показывает, что первые тесты закончились неудачей.

Теперь задача в том, чтобы выполнить тест с минимальными усилиями. Поэтому, определим пространство имен и настройки по умолчанию в нашем подключаемом модуле jQuery:

(function($) {
   var picasaWebViewer = $.picasaWebViewer = {};
    
   picasaWebViewer.defaultOptions = {
      urlFormat : "http://picasaweb.google.com/data/feed/api/user/{0}?alt=json-in-
         script",
      albumTitleMaxLength : 15,
      defaultDialogWidth : 600,
      defaultDialogHeight : 400 
   };
                    
   $.fn.picasaWebViewer = function(options) {
      return this.each(function() {            
      });
   };    
})(jQuery);

При повторном выполнении теста QUnit возвращает зеленую полосу успеха. (См. рис. 4.) В этот раз не пришлось переделывать слишком много, поэтому сфокусируемся на нескольких новых тестах.

Примечание. Чтобы упростить TDD, я сделал все функции в подключаемом модуле jQuery публичными. Это необязательно, но я считаю, что так проще полностью выполнить тестирование фрагмента кода. Разработчики могут с этим поспорить, но у данной статьи другая цель.


Рис.4. Зеленая полоса показывает, что тесты пройдены успешно.

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

test("Override Default Options", function() {
    $.picasaWebViewer.overrideOptions({
        urlFormat : 'http://www.google.com',
        albumTitleMaxLength : 25,
        defaultDialogWidth : 400,
        defaultDialogHeight : 300,
        userName : 'BillGates'      
    });
    
    same($.picasaWebViewer.options.urlFormat, 'http://www.google.com'); 
    same($.picasaWebViewer.options.albumTitleMaxLength, 25);    
    same($.picasaWebViewer.options.defaultDialogWidth, 400);    
    same($.picasaWebViewer.options.defaultDialogHeight, 300);   
    same($.picasaWebViewer.options.userName, 'BillGates');  
});

Как можно догадаться, эти тесты заканчиваются неудачей, так как я еще не определил функцию overrideOptions. Чтобы тесты выполнялись, мне понадобится определить метод и добавить в него немного логики, чтобы переопределить настройки по умолчанию. Вот этот код:

(function($) {
   var picasaWebViewer = $.picasaWebViewer = {};
    
   picasaWebViewer.defaultOptions = {
      urlFormat : "http://picasaweb.google.com/data/feed/api/user/{0}?alt=json-
         in-script",
      albumTitleMaxLength : 15,
      defaultDialogWidth : 600,
      defaultDialogHeight : 400 
   };
        
   picasaWebViewer.options = null;
    
   picasaWebViewer.overrideOptions = function(options) {
      picasaWebViewer.options = $.extend(
         {},
         picasaWebViewer.defaultOptions,           
         options);
   };
})(jQuery);

При повторном выполнении этих тестов я увижу зеленую полосу успеха.

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

test("Calling Plugin Override Options", function() {
   $("#targetId").picasaWebViewer({
      userName : "elijah.manor"
   });
    
   same($.picasaWebViewer.options.urlFormat,  
      'http://picasaweb.google.com/data/feed/api/user/{0}?alt=json-in-script');
   same($.picasaWebViewer.options.albumTitleMaxLength, 15);
   same($.picasaWebViewer.options.defaultDialogWidth, 600);
   same($.picasaWebViewer.options.defaultDialogHeight, 400);            
   same($.picasaWebViewer.options.userName, 'elijah.manor');    
});

Для использования подключаемого модуля мне также нужно добавить элемент в средство выполнения тестов index.html с id "targetId". Я добавил "display: none" в конечный элемент главным образом, чтобы не исказить результаты теста.

<body>
   <h1 id="qunit-header">QUnit Test Suite</h1>
   <h2 id="qunit-banner"></h2>
   <div id="qunit-testrunner-toolbar"></div>
   <h2 id="qunit-userAgent"></h2>
   <ol id="qunit-tests"></ol>
   <div id="qunit-target" style="display: none;"></div>
</body>

Выполнение этих тестов, конечно, заканчивается неудачей, так как я еще не определил подключаемый модуль jQuery. Теперь я сделаю это и вызову функцию overrideOptions.

$.fn.picasaWebViewer = function(options){
   picasaWebViewer.overrideOptions(options);                    

   return this;
};

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

Методы Setup и Teardown модуля

Сейчас нужно отметить важный момент, связанный с тестированием. Если я создал другой набор тестов, использующий одну из уже проверенных возможностей, я могу получить неожиданный результат. Проблема в том, что я не сбрасывал состояние подключаемого модуля между тестами. Следующим шагом является добавление в наш модуль функции setup.

module("Picasa Web Viewer", {
        setup: function() {     
            $.picasaWebViewer.overrideOptions({});
        },
        teardown: function() {
        }
    });

Можно передать модулю как функцию setup, так и функцию teardown, которые будут выполняться между каждым из тестов внутри модуля. Пока мы не используем функцию teardown, но мы задействуем ее в последующих тестах. В функции setup я восстанавливаю значения по умолчанию для параметров, если напрямую тестируется одна из публичных функций. Технически, добавляя в этот момент функции setup и teardown, я нарушил принципы TDD, но эти принципы большей частью являются рекомендациями, и каждый разработчик стремится изменить их под себя.

Теперь пришло время приступить к чему-то чуть более приятному, чем проверка настроек по умолчанию. Смысл этого подключаемого модуля — отображение альбомов и изображений, поэтому здесь я сфокусируюсь на создании соответствующих структур HTML для размещения альбомов и изображений.

var targetId = "#qunit-target";

test("Scafford Gallery", function() {
   $.picasaWebViewer.scaffoldGallery($(targetId));
   ok($(targetId).find('#gallery').length, 'Gallery created');
   ok($(targetId).find('#tabs').length, 'Tabs created');
});

Так как HTML-элемент еще понадобится нам для тестирования, я вставил targetId в его собственную переменную, чтобы ее можно было использовать в последующих и будущих тестах.

var tabs, gallery, 
   picasaWebViewer = $.picasaWebViewer = {};

picasaWebViewer.scaffoldGallery = function(element) {
   var html = 
      "<div class='demo ui-widget ui-helper-clearfix'>" +                
         "<div id='tabs'>" + 
            "<ul>" + 
               "<li><a href='#tabs-0'>Albums</a></li>" + 
            "</ul>" + 
            "<div id='tabs-0'>" + 
               "<ul id='gallery' " + 
                  "class='gallery ui-helper-reset ui-helper-clearfix' />" + 
            "</div>" + 
         "</div>" +                                                                   
      "</div>";
                                          
   tabs = $(html).appendTo(element).find('#tabs').tabs();
   gallery = $('#gallery');
};

Так как я добавил элементы в DOM средства выполнения тестов, мне нужно очистить их перед дальнейшими тестами. Я легко могу достичь этого, добавляя простую функцию jQuery, очищающую все элементы DOM от целевого элемента в средстве выполнения тестов.

module("Picasa Web Viewer", {
   setup: function() {  
      $(targetId).empty();  
      $.picasaWebViewer.overrideOptions({});
   },
   teardown: function() {
   }
});

Использование платформы макетов в тестах модулей

Пришла пора позабавиться на территории, неведомой многим разработчикам JavaScript. Я собираюсь погрузиться в макетирование. Мне нравится способ, предложенные Роем Ошероувом (Roy Osherove) в одном из его недавних сообщений в блоге:

"Макеты — это замаскированные шпионы в ваших тестах, это двойные агенты. Они позволяют делать то, что нужно так, чтобы фактический код не знал об этом, и сообщают обо все, что происходит с ними, например, "ваш класс *должен* был вызвать мой метод "authenticate" с параметрами x и Y, он он фактически вызвал его с неправильным значением… Ваш тест должен закончиться *неудачно*”."

Как уже говорилось, для макетирования JavaScript я использую QMock. Следующие тесты кода подтверждают, что функция getAlbums вызывается и никакие результаты не возвращаются, и что вызов displayAlbums не происходит. Этот тест также определяет, должна ли была вызываться функция scaffoldGallery. Так как мы тестировали эту публичную функцию раньше, ее повторное тестирование не требуется. Мы просто хотим знать, вызывалась ли она.

test("GetAndDisplayAlbums не возвращает ничего, поэтому альбомы не отображаются", function() {
   var target = $(targetId)[0];        
   var mockRepository = new Mock();
   mockRepository  
      .expects(1)
      .method('getAlbums')
      .withArguments(Function)
      .callFunctionWith(null);
            
   var mockPicasaWebViewer = new Mock();
   mockPicasaWebViewer  
      .expects(1)
      .method('scaffoldGallery')
      .withArguments(target);
   mockPicasaWebViewer  
      .expects(0)
      .method('displayAlbums');
                  
   $.picasaWebViewer.setRepository(mockRepository);   
   $.picasaWebViewer.setPicasaWebViewer(mockPicasaWebViewer);    
   $.picasaWebViewer.getAndDisplayAlbums(target);
        
   ok(mockRepository.verify(), 'Проверить, что getAlbums была вызвана'); 
   ok(mockPicasaWebViewer.verify(), 'Проверить, что displayAlbums не была вызвана'); 
});

В этом коде следует обратить внимание на пару моментов:

  1. Когда я макетирую функцию getAlbums, в качестве аргумента я передаю Function. Он представляет функцию обратного вызова, вызываемую, когда что-то передается в функцию callFunctionWith.
  2. Я ввел концепцию Репозитория. Я добавил ее для функций, которые будут использовать функцию jQuery AJAX. Я не хочу в действительности выполнять запрос AJAX, так как он является внешней зависимостью. Вместо этого, я выталкиваю его в другую переменную, чтобы я мог макетировать его независимо. Соответствующий пример я приведу позже в этой статье.
  3. Я вызываю две функции set, которые переопределяют поведение по умолчанию для подключаемого модуля jQuery. Вместо использования определенных экземпляров внутри подключаемого модуля я заменяю их объектами макетов, предназначенными для регистрации их поведения и возвращения того, что мне от них нужно. Нам нужно вставить эти функции set в наш подключаемый модуль.

Чтобы пройти этот тест, мне нужно написать новую функцию в наш подключаемый модуль jQuery, называющийся getAndDisplayAlbums. Эта функция фактически будет стартовой точкой для остальных функциональных возможностей подключаемого модуля в функции $.fn.picasaWebViewer.

var tabs, gallery, repository,
   picasaWebViewer = $.picasaWebViewer = {};

picasaWebViewer.getAndDisplayAlbums = function(element) {
   console.group('getAndDisplayAlbums');
   picasaWebViewer.scaffoldGallery(element);
   repository.getAlbums(function(albums) {            
      if (albums) {
         picasaWebViewer.displayAlbums(albums);
      } 
   });
   console.groupEnd('getAndDsiplayAlbums');         
};

picasaWebViewer.setRepository = function(object) {
   repository = object; 
};
    
picasaWebViewer.setPicasaWebViewer = function(object) {
   picasaWebViewer = object;    
};

Теперь тесты выполняются, так как я заставил функцию getAlbums возвращать значение null в функцию обратного вызова, которая никогда не вызывает функцию displayAlbums. Вуа-ля!

И, так как мы переопределили поведение по умолчанию для Repository и PicasaWebViewer, перед выполнением остального набора тестов необходимо вернуть их исходное состояние обратно.

module("Picasa Web Viewer", {
   setup: function() {   
        $(targetId).empty();    
        $.picasaWebViewer.overrideOptions({});
   },
   teardown: function() {
      $.picasaWebViewer.setJquery(oldJquery);
      $.picasaWebViewer.setPicasaWebViewer(oldPicasaWebViewer);         
   }
});

Давайте повторим макетирование, но в этот раз для функции jQuery.ajax. Я напишу тест для функции getAlbums, которая использует JSONP для отправки запроса в веб-интерфейс API Google Picasa, чтобы получить данные альбома.

test("Репозиторий GetAlbums", function() {    
   $.picasaWebViewer.overrideOptions({
      urlFormat : 'http://www.google.com/{0}',
      userName : 'BillGates'       
   })        
       
   var mockJquery = new Mock();
      mockJquery
         .expects(1)
         .method('ajax')
         .withArguments({
            url: 'http://www.google.com/BillGates',
            success: Function,
            dataType: "jsonp" 
         })
         .callFunctionWith({ feed : { entry : "data response" }});
        
   $.picasaWebViewer.setJquery(mockJquery);
   var albums = null;
   $.picasaWebViewer.repository.getAlbums(function(data) {
      albums = data;        
   });    
        
   ok(albums, "Данные альбома были возвращены");
   same(albums, "data response");
   ok(mockJquery.verify(), 'Проверить, что ajax был вызван'); 
});

В этом тесте я проверяю, была ли вызвана функция jQuery.ajax один раз, и что при успешном запуске обратного вызова jQuery.ajax со сложным объектом JSON функция getAlbums выполнит разбор JSON и возвратит только свойство feed.entry. Код для создания этого теста приведен ниже:

picasaWebViewer.repository = {
   getAlbums : function(callback) {
      console.group('getAlbums');
        
      var updatedUrl = picasaWebViewer.options.urlFormat.replace("{0}",
         picasaWebViewer.options.userName);
      $.ajax({ 
         url: updatedUrl,
         success: function(data) {
            callback(data.feed.entry); 
         },
         dataType: 'jsonp'
      });
            
      console.groupEnd('getAlbums');
   }
};

До сих пор во всех макетируемых объектах я ожидал выполнения либо 0, либо 1 вызов. Теперь протестируем функцию displayAblums, вызывая несколько функций displayAlbum.

var testAlbum = {
   title : {
      $t : "myTitle"
   },
   media$group : {
      media$thumbnail : [{url : "myUrl"}]
   },
   link: [{href : "myHref"}]
};
 
test("Отобразить альбомы", function() {
   var mockPicasaWebViewer = new Mock();
   mockPicasaWebViewer  
      .expects(2)
      .method('displayAlbums');
      .withArguments(testAlbum);    
   $.picasaWebViewer.setPicasaWebViewer(mockPicasaWebViewer);    
    
   $.picasaWebViewer.displayAlbums([testAlbum, testAlbum]); 
    
   ok(mockPicasaWebViewer.verify(), 'Проверить, что displayAlbum была вызвана дважды'); 
});

В этом тесте я вызываю функцию displayAlbums, передаю ей массив тестовых альбомов, и в макетируемом объекте ожидаю, что функция displayAlbum будет вызвана дважды, в соответствии с количеством альбомов. Чтобы тест выполнялся успешно, в подключаемый модуль можно добавить следующую функцию:

picasaWebViewer.displayAlbums = function(albums) {
   console.group('displayAlbums');
   $.each(albums, function() {
      picasaWebViewer.displayAlbum(this);
   });
   console.groupEnd('displayAlbums'); 
};

Следующим шагом является тестирование, правильно ли альбомы отображаются на странице. В этом тесте я, используя свойства альбома, проверяю, точно ли альбом, переданный функции displayAlbum, отражается в модели DOM .

test("Отобразить альбом", function() {
   //Упорядочить
   $.picasaWebViewer.scaffoldGallery($(targetId)[0]);     
        
   //Действовать
   $.picasaWebViewer.displayAlbum(testAlbum); 
    
   //Подтвердить
   ok($('#gallery').find('li.ui-widget-content').length, 'Найден элемент списка');    
   ok($('#gallery').find('h5').text() === testAlbum.title.$t, 'Заголовок соответствует');
   ok($('#gallery').find('img').attr('src') === testAlbum.media$group.media$thumbnail[0].url, 'Url эскиза соответствует');
});

Примечание. Я повторно использую закрытую переменную testAlbum, определенную в предыдущем тесте.

Для выполнения предыдущих тестов мне фактически нужно определить функцию displayAlbum, показанную ниже:

picasaWebViewer.displayAlbum = function(album) {
   console.group('displayAlbum');
        
   var title = picasaWebViewer.truncateTitle(album.title.$t);
   var html = 
      "<li class='ui-widget-content ui-corner-tr'>" +
         "<h5 class='ui-widget-header'>" + title + "</h5>" +
         "<a><img src='" + album.media$group.media$thumbnail[0].url + "' alt='" + 
            album.title.$t + "' /></a>" +          
       "</li>"; 
                    
   $(html)
      .appendTo(gallery)
      .children("a").attr('href', album.link[0].href)
      .click(picasaWebViewer.clickAlbum);              
   console.groupEnd('displayAlbum');
};

Я не хочу представлять в этой статье всю программу, но оставшуюся часть моих тестов и подключаемого модуля jQuery можно просмотреть на странице GitHub. Кроме того, если интересно, можно запустить подключаемый модуль jQuery на моем веб-сайте.

Покрытие кода

Теперь я хочу перейти к другой теме. Как упоминалось выше, JSCoverage — это средство покрытия кода, которое я использую при тестировании модулей. Проект какое-то время не обновлялся, но, по моему опыту, он все равно хорошо работает. После загрузки последней версии с веб-сайта JSCoverage, следует просмотреть документацию о том, с чего начать оценку покрытия своего кода.

Начнем с того, что мой проект-пример находится в папке picasaWebViewer, и я помещу папку jscoverage-0.4 в ту же родительскую папку, как показано на рис. 5.


Рис. 5. Файловая структура перед созданием папки JSCoverage

Далее запускается средство командной строки jscoverage, в которое передается путь к исходному коду, а затем путь к папке, в которой программа JSCoverage должна создать свой специальный каталог, где и будет выполняться тестирование.

C:\...\jscoverage-0.4>jscoverage ../picasaWebViewer ../picasaWebViewerInstrumented

После выполнения программы командной строки появляется папка с именем picasaWebViewerInstrumented, которая фактически является копией первоначальной папки исходного кода плюс несколько добавленных JSCoverage полезностей. Теперь структура папок должна выглядеть так, как показано на рис. 6.


Рис. 6. Структура папок после выполнения программы командной строки JSCoverage

Внутри новой папки Instrumented находится файл с именем jscoverage.html, который можно запустить, чтобы начать анализ покрытия кода. Далее введите URL-адрес своего html-файла QUnit, а затем нажмите кнопку "Go" (Вперед), чтобы начать анализ своего кода. Результаты будут похожи на рис. 7.


Рис.7. Результаты покрытия кода в JSCoverage

После выполнения всех тестов щелкните вкладку Summary (Сводка), чтобы просмотреть сведения о том, какие файлы выполнялись, сколько операторов было выполнено и каков процент покрытия тестами. Пример приведен на рис. 8.


Рис. 8. Сводка анализа покрытия кода

Процент покрытия кода можно просмотреть для каждого файла (представлен в качестве индикатора выполнения) в правом столбце. В нашем примере покрытие кода составляет 100 процентов, то есть наши тесты модулей могут выполнить каждую строку в нашем подключаемом модуле jQuery.

Примечание. В начале работы с тестированием модулей и покрытием кода может возникнуть стремление добиться 100-процентного покрытия для тестов, но это не является обязательным. Смысл в том, чтобы максимально протестировать важные функции и требования для уверенности и крайних случаев, но возможны ситуации, в которых усилия по обеспечению большего покрытия не оправдывают себя. Предполагается, что тестирование модулей поможет убедиться в правильности работы кода и сделает его оптимизацию намного проще и намного надежнее .

Если покрытие кода не является 100-процентным, можно использовать некоторые полезные возможности JSCoverage, помогающие просмотреть непокрытый код. Чтобы увидеть, как это работает, давайте закомментируем один из наших тестов модулей и снова запустим средство покрытия кода, но в этот раз мы установим флажок "Show Missing Statements Column" (Показать столбец пропущенных операторов). Результаты показаны на рис. 9.


Рис. 9. Проверить непротестированные строки можно, щелкая их.

Теперь можно видеть, что покрытие кода составляет только 97 процентов, и что столбец "Missing" (Пропущенные) показывает, какие строки не были протестированы. Щелкнув один из связанных номеров, можно просмотреть исходный код и увидеть, какие строки были выполнены, как показано на рис. 10.


Рис. 10. Можно видеть, какие строки кода были выполнены в тестах модулей.

Видны не только выделенные красным строки кода, которые не были выполнены в наших тестах модулей, но и сколько раз был выполнены другие строки кода, выделенные зеленым.

Примечание. Если решено написать дополнительные тесты модулей, чтобы охватить строки, показанные красным, понадобится заново выполнить средство командной строки JSCoverage, чтобы заново создать папку Instrumented для повторного тестирования покрытия кода.

Заключение

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