Разработка на основе тестирования: "сверху вниз" или "снизу вверх"?

Разработка на основе тестирования — это процесс программирования, в котором модульные тесты используются для определения поведения системы перед написанием фактической реализации. Разработка на основе тестирования для JavaScript была рассмотрена в ScriptJunkie ранее автором Элайджа Манор (Elijah Manor). В данной статье я хочу обсудить различие между разработкой снизу вверх и сверху вниз.

Разработка "снизу вверх"

Разработка "снизу вверх" — это традиционно наиболее распространенный способ осуществления разработки на основе тестирования. Выбор подхода "снизу вверх" соответствует началу с независимых низкоуровневых объектов и функций, таких как настраиваемый интерфейс AJAX, построенный на базе такой библиотеки, как jQuery, которая специально разработана для внутренних ресурсов вашего приложения, например, blogPost.get(3245, callback). Подключаемый модуль jQuery для просмотра веб-альбомов Picasa, созданный Элайджей в упомянутой статье, был разработан снизу вверх. Он начинался с определения механики подключаемого модуля с последующим добавлением компонентов до того момента, пока этот подключаемый модуль не смог отображать изображения.

Разработка "сверху вниз"

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

Разработка "сверху вниз", а затем "снизу вверх"

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

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

Оговорка: я не говорю о том, что вы не можете получить слабо связанные элементы при разработке "сверху вниз". Я говорю о том, что подход "сверху вниз" позволяет получить гораздо более естественный результат.

Поставленная задача: поиск в режиме реального времени

Чтобы проиллюстрировать такой смешанный подход, я покажу вам примеры из разработки подключаемого модуля поиска в режиме реального времени для jQuery на основе тестирования. Для тех, кто не знаком с "поиском в режиме реального времени" (или "автозаполнением/автозавершением"), отметим, что это улучшение, которое можно применить, например, к формам поиска. По мере ввода пользователем под текстовым полем раскрывается список предложенных терминов, а элементы в этом списке можно выбирать для упрощения поиска требуемого элемента.

Первый тест: высокоуровневый обзор

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

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

TestCase("LiveSearchFunctionalTest", {
    "test should display suggestions as user types": function () {
    }
});

Добавление разметки

Далее нам нужна форма для улучшения. Я запущу тесты с использованием JsTestDriver, который позволяет не использовать для запуска тестов файлы вспомогательного кода HTML, а создать их на сервере. В большинстве случаев отказ от вспомогательного кода HTML приносит пользу, поскольку сокращает объем организационных действий для написания и запуска тестов. Однако это значит, что нам нужно работать немного упорнее, чтобы внедрить образец разметки для использования тестами.

Чтобы упростить создание образца разметки для тестов, JsTestDriver предоставляет компонент "HTMLDoc", который позволяет внедрить некоторую разметку в комментарий внутри теста. Этот комментарий анализируется на стороне сервера JsTestDriver, а итоговые узлы DOM назначаются свойству в тестовом случае (например, только в памяти, что работает быстрее) или прикрепляются к документу. Мы не хотим, чтобы подключаемый модуль поиска в режиме реального времени считал, что он является владельцем документа, поэтому помещение в память вполне подходит:

TestCase("LiveSearchFunctionalTest", {
    "test should display suggestions as user types": function () {
        /*:DOC form = <form action="/search" method="get">
            <fieldset>
              <input type="text" name="q">
              <input type="submit" value="Go!">
            </fieldset>
          </form>*/

        // this.form now holds the form element
    }
});

Инициализация модуля поиска в режиме реального времени

После размещения формы нам необходимо инициализировать компонент. Это будет единственным случаем взаимодействия с нашим API в рамках этого высокоуровневого теста.

TestCase("LiveSearchFunctionalTest", {
    "test should display suggestions as user types": function () {
        /*:DOC form = <form action="/search" method="get">
            <fieldset>
              <input type="text" name="q">
              <input type="submit" value="Go!">
            </fieldset>
          </form>*/

        var form = jQuery(this.form);
        form.liveSearch();
    }
});

Имитация ввода пользователя

Когда модуль настроен, нам необходимо ввести некоторые сведения и затем подтвердить, что выполняются правильные операции. Мы активируем только интересующие нас события ввода, в данном случае это событие keyup. Давайте предположим, что наша форма осуществляет поиск по базе данных фильмов и мы хотим найти фильм "Robocop". Поскольку мы не хотим, чтобы наш сервер обрабатывал отдельные запросы для каждого нажатия клавиши, мы установим принудительное минимальное время ожидания перед обращением к серверу. Итоговый код выглядит следующим образом:

var input = form.find("input[type=text]");

input.val("R");
input.trigger("keyup");
// Small delay, user cannot type instantaneously

input.val("Ro");
input.trigger("keyup");
// Small delay

input.val("Rob");
input.trigger("keyup");
// Small delay

input.val("Robo");
input.trigger("keyup");
// Small delay

input.val("Roboc");
input.trigger("keyup");
// Small delay

input.val("Roboco");
input.trigger("keyup");
// Small delay

input.val("Robocop");
input.trigger("keyup");
// Longer delay

Эта часть довольно подробна. Мы может очистить это позднее, но перед прохождением теста; введение сложной логической схемы только повышает риск возникновения ошибок во время теста.

Ход времени

В приведенном выше фрагменте кода отсутствует одна вещь. Хотя Робокоп мог бы вводить текст со скоростью быстрее 1 миллисекунды, люди такого не могут, и это должно быть отражено в тесте. Кроме того, нам нужны задержки, чтобы убедиться, что запросы не будут отправлены до истечения минимального времени ожидания.

Вместо того, чтобы заставлять тест ожидать после каждого нажатия клавиши, что было бы медленно, мы будем использовать библиотеку Sinon.JS для искусственной передачи времени:

TestCase("LiveSearchFunctionalTest", sinon.testCase({
    "test should display suggestions as user types": function () {
        // Setup as before
        // ...

        input.val("R");
        input.trigger("keyup");
        this.clock.tick(67);

        input.val("Ro");
        input.trigger("keyup");
        this.clock.tick(98);

        // ...

        input.val("Robocop");
        input.trigger("keyup");
        this.clock.tick(150);
    }
}));

Создание для тестового случая оболочки из вызова в sinon.testCase позволяет производить отсчет времени для каждого из тестов локально. Метод clock.tick(ms) позволяет обеспечить правильный запуск всех таймеров, запуск которых запланирован (с помощью setTimeout или setInterval) в течение следующих ms миллисекунд. После последнего нажатия клавиши мы "ждем" 150 миллисекунд, что соответствует необходимому времени ожидания. В этот момент наш код должен обратиться к серверу.

Подтверждение отображения предложений

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

var results = form.find("ol.live-search-results");
assertEquals(??, results.length);
assertEquals("???", results.get(0).innerHTML);
// ...

Это основа проверки теста. Однако у нас еще имеются не все сведения, поэтому пока невозможно узнать, сколько результатов следует ожидать или каково их содержимое. Для ответа на этот вопрос нам придется иметь дело с запросом к серверу.

Тестирование взаимодействия с сервером

По причинам, указанным в моей предыдущей статье ScriptJunkie, которая посвящена заглушкам и макетам, непосредственное подключение к рабочему серверу в рамках теста нежелательно. Это значит, что мы воспользуемся библиотекой Sinon.JS и при тестировании сервера.

Запросы сервера-заполнителя обрабатываются библиотекой Sinon.JS в два прохода. Сначала мы определяем ответы, о которых должно быть известно серверу. Потом мы велим серверу отвечать на все запросы в удобное время. Мы можем определить ресурсы сервера в любое время, но поскольку это часть настройки теста, имеет смысл сгруппировать эту процедуру с остальным кодом настройки в начале теста:

TestCase("LiveSearchFunctionalTest", sinon.testCase({
    "test should display suggestions as user types": function () {
        /*:DOC form = ... */

        this.server.respondWith(
            "GET", "/search?q=Robocop",
            [200, { "Content-Type": "application/json" },
             '["Robocop", "Robocop 2", "Robocop 3"]']
        );

        // ...
    }
}));

Здесь мы сообщаем серверу, как обрабатывать запросы GET для /search?q=Robocop, а именно, используя для ответа массив JSON из строк (например, заголовки фильмов). Как только пользователь закончил ввод и прошло минимальное время ожидания, равное 150 миллисекундам, мы велим серверу отвечать на все запросы с помощью вызова this.server.respond:

input.val("Robocop");
input.trigger("keyup");
this.clock.tick(150);

this.server.respond();

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

var results = form.find("ol.live-search-results");
assertEquals(3, results.length);
assertEquals("Robocop", results.get(0).innerHTML);
assertEquals("Robocop 2", results.get(1).innerHTML);
assertEquals("Robocop 3", results.get(2).innerHTML);

Потребность во времени ожидания

В нашем тесте все еще отсутствует одна деталь. Похоже, что мы ожидаем отсутствие запросов к серверу до того момента, когда пользователь прекратил ввод хотя на 150 миллисекунд. Однако проверочные утверждения, которые проверяют такое поведение, в нашем тесте отсутствуют. Чтобы выполнить данное требование, мы добавим еще одно проверочное утверждение сразу после последнего нажатия клавиши, но перед истечение финального временем ожидания, для подтверждения отсутствия запросов в течение указанного времени:

input.val("Robocop");
input.trigger("keyup");
assertEquals(0, this.server.requests.length);

this.clock.tick(150);

После этого тест завершен, и в нем легко просматриваются автоматические предложения:

TestCase("LiveSearchFunctionalTest", sinon.testCase({
    "test should display suggestions as user types": function () {
        /*:DOC form = <form action="/search" method="get">
            <fieldset>
              <input type="text" name="q">
              <input type="submit" value="Go!">
            </fieldset>
          </form>*/

        var form = jQuery(this.form);
        form.liveSearch();
        var input = form.find("input[type=text]");

        input.val("R");
        input.trigger("keyup");
        this.clock.tick(67);

        input.val("Ro");
        input.trigger("keyup");
        this.clock.tick(98);

        input.val("Rob");
        input.trigger("keyup");
        this.clock.tick(69);

        input.val("Robo");
        input.trigger("keyup");
        this.clock.tick(103);

        input.val("Roboc");
        input.trigger("keyup");
        this.clock.tick(82);

        input.val("Roboco");
        input.trigger("keyup");
        this.clock.tick(112);

        input.val("Robocop");
        input.trigger("keyup");
        assertEquals(0, this.server.requests.length);

        this.clock.tick(150);
        this.server.respond();

        var results = form.find("ol.live-search-results");
        assertEquals(3, results.length);
        assertEquals("Robocop", results.get(0).innerHTML);
        assertEquals("Robocop 2", results.get(1).innerHTML);
        assertEquals("Robocop 3", results.get(2).innerHTML);
    }
}));

Запуск теста

Чтобы запустить тест, необходим небольшой файл конфигурации для JsTestDriver, сохраните его в jsTestDriver.conf:

server: http://localhost:4224

load:
  - test/*.js

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

server: http://localhost:4224

load:
  - lib/*.js
  - src/*.js
  - test/*.js

Где каталог lib содержит Sinon.JS и jQuery, а каталог src содержит отдельный файл live_search.jquery.js со следующим содержимым:

jQuery.fn.liveSearch = function () {};

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

Проход "снизу вверх": время ожидания

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

Начиная с малого: организация очереди запросов в нужное время

Как и всегда при проведении разработки на основе тестирования мы начнем с самого простого теста, который только можно себе представить. Этот тест будет ожидать, что сразу же после помещения запроса в очередь запрос не выполняется:

TestCase("LiveSearchTest", sinon.testCase({
    "test queuing query should not immediately send request": function () {
        var liveSearch = new LiveSearch();
        liveSearch.queue("Movie");

        assertEquals(0, this.server.requests.length);
    }
}));

Повторим, что мы используем Sinon.JS для предоставления сервера-заполнителя, а тест проверяет наличие 0 запросом для сервера после вызова метода queue еще несуществующего объекта LiveSearch. Тест завершается со сбоем, а прохождение его зависит от определения конструктора и метода queuesrc/live_search.js):

function LiveSearch() {}
LiveSearch.prototype.queue = function () {};

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

"test should send request after minimum timeout": function {
    var liveSearch = new LiveSearch();
    liveSearch.queue("Movie");
    this.clock.tick(150);

    assertEquals(1, this.server.requests.length);
}

Тест не требует от нас многого. Он ожидает отправки одного запроса — любого на сервер. Пройти его очень просто:

LiveSearch.prototype.queue = function () {
    setTimeout(function () {
        jQuery.ajax();
    }, 150);
};

Хотя и видно, что данная реализация неполна, но она удовлетворяет условиям теста.

Концентрация внимания на времени ожидания

Предыдущий тест показал, что что-то назревает. Как и в случае с предыдущим функциональным тестом добавление значимых проверочных утверждений в тест является трудной задачей. В идеальном случае нам бы хотелось проверить, что URL-адрес запроса содержит в некоторой форме строку запроса. К сожалению, из теста неочевидно, как именно должен выглядеть URL-адрес, каким должно быть имя параметра запроса (при наличии) и даже какой именно запрос должен использоваться — GET или POST.

Одна из проблем с предыдущим тестом заключается в том, что он не получает достаточно входных данных, чтобы мы смогли узнать, как и где будет выполнен запрос. Более значительная проблема заключается в том, что из-за такого недостатка информации мы отклоняемся от первоначальной цели: моделирования времени ожидания. Один возможный вывод показывает, что от LiveSearch сейчас ожидаются две вещи: обработка времени ожидания и обработка запросов к серверу. Нам будет проще рассуждать о времени ожидания, если мы сможем переложить взаимодействие с сервером на кого-то другого. Поэтому мы выполним рефакторинг имеющегося у нас малого объема кода, чтобы делегировать работу с сетью вспомогательному обработчику:

TestCase("LiveSearchTest", sinon.testCase({
    setUp: function () {
        this.liveSearch = new LiveSearch();

        this.liveSearch.dataSource = {
            get: sinon.spy()
        };
    },

    "test queuing query should not immediately perform search": function () {
        this.liveSearch.queue("Movie");

        sinon.assert.notCalled(this.liveSearch.dataSource.get);
    },

    "test should perform search after minimum timeout": function {
        var ls = this.liveSearch;
        ls.onData = function () {};
        ls.queue("Movie");
        this.clock.tick(150);

        sinon.assert.calledOnce(ls.dataSource.get);
        sinon.assert.calledWith(ls.dataSource.get, "Movie", ls.onData);
    }
}));

Теперь тест ожидает, что объект liveSearch получит данные через вспомогательного обработчика dataSource. Это мелкое изменение значительно упрощает проверку использования запроса при запросе результатов. Оно также упрощает использование логической схемы времени ожидания с любым источником данных — резервным jQuery.ajax, WebSockets или даже основанным на JSON-P. Кроме того, обратите внимание на использование sinon.assert.*, который ведет себя как и встроенные проверочные утверждения JsTestDriver за исключением лучше настроенных сообщений об ошибках для тестовых наблюдателей.

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

LiveSearch.prototype.queue = function (query) {
    var self = this;

    setTimeout(function () {
        self.dataSource.get(query, self.onData);
    }, 150);
};

Несколько помещенных в очередь запросов

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

"test should extend minimum timeout": function () {
    this.liveSearch.queue("Robocop");
    this.clock.tick(100);
    this.liveSearch.queue("Terminator");
    this.clock.tick(50);

    sinon.assert.notCalled(this.liveSearch.dataSource.get);
}

Тест ждет в общей сложности 150 миллисекунд, но поскольку с момента прошлого запроса прошло всего 50 миллисекунд, мы ожидаем отсутствие выполненного запроса. Тест завершается со сбоем, так как запрос "Robocop" все еще передается в источник данных. Решение заключается в очистке всех имеющихся периодов времени ожидания при каждом помещении запроса в очередь:

LiveSearch.prototype.queue = function (query) {
    if (this.timerId != null) {
        clearTimeout(this.timerId);
    }

    var self = this;

    this.timerId = setTimeout(function () {
        self.dataSource.get(query, self.onData);
    }, 150);
};

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

"test should discard old queued queries": function () {
    this.liveSearch.queue("Robocop");
    this.clock.tick(100);
    this.liveSearch.queue("Terminator");
    this.clock.tick(150);

    sinon.assert.calledWith(this.liveSearch.dataSource.get, "Terminator");
}

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

Отклонение от благоприятного пути

До сих пор мы тестировали только благоприятный путь для объекта поиска в режиме реального времени. Сейчас мы напишем тест, который ожидает отсутствие запроса данных для пустых запросов:

"test should not make requests for empty queries": function () {
    this.liveSearch.queue("");
    this.clock.tick(150);

    sinon.assert.notCalled(this.liveSearch.dataSource.get);
}

При текущей реализации queue данный тест завершается со сбоем. Чтобы пройти его, мы можем прервать метод в случае пустого запроса:

LiveSearch.prototype.queue = function (query) {
    if (!query) {
        return;
    }

    if (this.timerId) {
        clearTimeout(this.timerId);
    }

    var self = this;

    this.timerId = setTimeout(function () {
        self.dataSource.get(query, self.onData);
    }, 150);
};

У меня такое чувство, что существенное значение имеет то, где мы прерываем метод queue. Чтобы проверить это, мы напишем последний тест, который будет ожидать, что предыдущие запросы прерываются, даже если новый запрос пуст и не активирует запрос к серверу:

"test should abort previous query even if new is empty": function () {
    this.liveSearch.queue("Robocop");
    this.liveSearch.queue("");
    this.clock.tick(150);

    sinon.assert.notCalled(this.liveSearch.dataSource.get);
}

Мои подозрения подтвердились — этот тест завершается со сбоем. Чтобы исправить ситуацию, мы просто переносим проверку аргумента на время после отмены таймера:

LiveSearch.prototype.queue = function (query) {
    if (this.timerId) {
        clearTimeout(this.timerId);
    }

    if (!query) {
        return;
    }

    var self = this;

    this.timerId = setTimeout(function () {
        self.dataSource.get(query, self.onData);
    }, 150);
};

Источник данных и отображение

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

"Готовый" подключаемый модуль

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

jQuery.fn.liveSearch = function () {
    this.each(function () {
        var form = jQuery(this);
        var input = form.find("input[type=text]");
        var liveSearch = new LiveSearch();

        liveSearch.dataSource = new XHRDataSource({
            url: this.action,
            method: this.method,
            param: input.attr("name")
        });

        var renderer = new ListRenderer(this);
        liveSearch.onData = function (data) {
            renderer.render(data);
        };

        input.bind("keyup", function () {
            liveSearch.queue(this.value);
        });
    });
};

Если вам интересно, как я выбирал этапы при разработке на основе тестирования для двух оставшихся объектов, можете ознакомиться с полным проектом на GitHub.

Функциональный тест после рефакторинга

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

TestCase("LiveSearchFunctionalTest", sinon.testCase({
    setUp: function () {
        this.type = function(input, text) {
            var characters = text.split("");
            var str = "";
            
            for (var i = 0, l = characters.length; i < l; ++i) {
                str += characters[i];
                input.val(str);
                input.trigger("keyup");
                this.clock.tick(90);
            }
        }
    },

    "test should display suggestions as user types": function () {
        /*:DOC form = <form action="/search" method="get">
            <fieldset>
              <input type="text" name="q">
              <input type="submit" value="Go!">
            </fieldset>
          </form>*/

        this.server.respondWith(
            "GET", "/search?q=Robocop",
            [200, { "Content-Type": "application/json" },
             '["Robocop", "Robocop 2", "Robocop 3"]']
        );

        var form = jQuery(this.form);
        form.liveSearch();

        var input = form.find("input[type=text]");
        this.type(input, "Robocop", this.clock);
        assertEquals(0, this.server.requests.length);

        this.clock.tick(150);
        this.server.respond();

        results = form.find("ol.live-search-results li");
        assertEquals(3, results.length);
        assertEquals("Robocop", results.get(0).innerHTML);
        assertEquals("Robocop 2", results.get(1).innerHTML);
        assertEquals("Robocop 3", results.get(2).innerHTML);
    }
}));

Заключение

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

Данный процесс набирает популярность, а представленное здесь его описание приближается к понятию, которое часто называют разработкой на основе приемочного тестирования (Acceptance test-driven development — ATDD). Обратите внимание на то, что функциональный тест, с которого мы начали, необязательно является тестом приемки, так как он проверяет только части компонента (то есть отсутствует навигация с помощью клавиатуры, отсутствуют обработчики щелчков для предложений и так далее). Но все равно он иллюстрирует, как учет функциональных возможностей для пользователя помогает нам выбирать правильные объекты для реализации, даже когда мы добираемся до самых основ.