Статьи

Разработка JavaScript на тестовой основе на практике

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

Переизданный учебник

Каждые несколько недель мы пересматриваем некоторые из любимых постов нашего читателя на протяжении всей истории сайта. Этот учебник был впервые опубликован в ноябре 2010 года.


В традиционном программировании проблемы решаются программированием, пока концепция полностью не представлена ​​в коде. В идеале код следует некоторым общим соображениям архитектурного проектирования, хотя во многих случаях, возможно, особенно в мире JavaScript, это не так. Этот стиль программирования решает проблемы, угадывая, какой код требуется для их решения, — стратегию, которая может легко привести к раздутым и тесно связанным решениям. Если также нет модульных тестов, решения, созданные с помощью этого подхода, могут даже содержать код, который никогда не выполняется, такой как логика обработки ошибок и «гибкая» обработка аргументов, или он может содержать крайние случаи, которые не были полностью протестированы, если тестировались совсем.

Разработка через тестирование переворачивает цикл разработки с ног на голову. Вместо того, чтобы сосредоточиться на том, какой код требуется для решения проблемы, разработка на основе тестирования начинается с определения цели. Модульные тесты формируют как спецификацию, так и документацию о том, какие действия поддерживаются и учитываются. Конечно, целью TDD не является тестирование, и поэтому нет гарантии, что он лучше обрабатывает, например, крайние случаи. Однако, поскольку каждая строка кода тестируется представительным фрагментом примера кода, TDD, вероятно, будет производить меньше избыточного кода, а функциональность, которая учитывается, вероятно, будет более устойчивой. Правильная разработка через тестирование гарантирует, что система никогда не будет содержать код, который не выполняется.


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

  • Написать тест
  • Запустите тесты, посмотрите новый тест не пройден
  • Сделать тестовый проход
  • Рефакторинг для удаления дублирования

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


Шаблон Observer (также известный как Publish / Subscribe или просто pubsub ) — это шаблон проектирования, который позволяет нам наблюдать за состоянием объекта и получать уведомления при его изменении. Шаблон может предоставить объектам мощные точки расширения при сохранении слабой связи.

В Обозревателе есть две роли — наблюдаемая и наблюдатель. Наблюдатель — это объект или функция, которая будет уведомлена, когда состояние наблюдаемого изменится. Наблюдаемая решает, когда обновлять своих наблюдателей и какие данные им предоставлять. Observable обычно предоставляет как минимум два открытых метода: pubsub , который уведомляет своих наблюдателей о новых данных, и pubsub который подписывает наблюдателей на события.


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


В этом примере для запуска тестов используется JsTestDriver. Руководство по установке доступно с официального веб-сайта.

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

1
2
3
4
5
6
7
chris@laptop:~/projects/observable $ tree
.
|— jsTestDriver.conf
|— src
|
`— test
    `— observable_test.js

Файл конфигурации — это просто минимальная конфигурация JsTestDriver :

1
2
3
4
5
server: http://localhost:4224
 
load:
  — lib/*.js
  — test/*.js

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


Первый тест попытается добавить наблюдателя, вызвав метод addObserver . Чтобы убедиться, что это работает, мы будем тупыми и предположим, что наблюдаемая хранит своих наблюдателей в массиве и проверим, что наблюдатель является единственным элементом в этом массиве. Тест находится в test/observable_test.js и выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
TestCase(«ObservableAddObserverTest», {
  «test should store function»: function () {
    var observable = new tddjs.Observable();
    var observer = function () {};
 
    observable.addObserver(observer);
 
    assertEquals(observer, observable.observers[0]);
  }
});

На первый взгляд, результат нашего самого первого теста является разрушительным:

1
2
3
4
5
6
7
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
tddjs is not defined
      /test/observable_test.js:3
 
Tests failed.

Не бойся! Неудача на самом деле хорошая вещь: она говорит нам, на чем следует сосредоточить наши усилия. Первая серьезная проблема заключается в том, что tddjs не существует. Давайте добавим объект пространства имен в src/observable.js :

1
var tddjs = {};

Повторный запуск тестов приводит к новой ошибке:

1
2
3
4
5
6
7
8
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
tddjs.Observable is not a constructor
      /test/observable_test.js:3
 
Tests failed.

Мы можем исправить эту новую проблему, добавив пустой конструктор Observable:

1
2
3
4
5
6
7
var tddjs = {};
 
(function () {
  function Observable() {}
 
  tddjs.Observable = Observable;
}());

Повторное выполнение теста приводит нас непосредственно к следующей проблеме:

1
2
3
4
5
6
7
8
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
 observable.addObserver is not a function
      /test/observable_test.js:6
 
Tests failed.

Давайте добавим отсутствующий метод.

1
2
3
function addObserver() {}
 
Observable.prototype.addObserver = addObserver;

При использовании метода тест завершается неудачей вместо отсутствующего массива наблюдателей.

1
2
3
4
5
6
7
8
E
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms)
    ObservableAddObserverTest.test should store function error (0.00 ms): \
observable.observers is undefined
      /test/observable_test.js:8
 
Tests failed.

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

1
2
3
4
5
6
7
8
9
function addObserver(observer) {
  this.observers = [observer];
}
 
Success!
 
.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms)
  Firefox 3.6.12 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)

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

Текущая реализация имеет две проблемы, с которыми мы должны иметь дело. Тест делает подробные предположения о реализации Observable, а реализация addObserver жестко запрограммирована в нашем тесте.

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

1
2
3
4
5
6
7
8
9
«test should store function»: function () {
  var observable = new tddjs.Observable();
  var observers = [function () {}, function () {}];
 
  observable.addObserver(observers[0]);
  observable.addObserver(observers[1]);
 
  assertEquals(observers, observable.observers);
}

Как и ожидалось, тест сейчас не проходит. Тест предполагает, что функции, добавленные в качестве наблюдателей, должны складываться как любой элемент, добавленный в pubsub . Для этого мы переместим создание экземпляра массива в конструктор и просто делегируем addObserver методу array push:

1
2
3
4
5
6
7
function Observable() {
  this.observers = [];
}
 
function addObserver(observer) {
  this.observers.push(observer);
}

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


Мы добавим еще один метод в Observable, hasObserver , и используем его для удаления некоторых помех, которые мы добавили при реализации addObserver .


Новый метод начинается с нового теста, а следующий hasObserver поведения для метода hasObserver .

01
02
03
04
05
06
07
08
09
10
TestCase(«ObservableHasObserverTest», {
  «test should return true when has observer»: function () {
    var observable = new tddjs.Observable();
    var observer = function () {};
 
    observable.addObserver(observer);
 
    assertTrue(observable.hasObserver(observer));
  }
});

Мы ожидаем, что этот тест провалится перед лицом отсутствующего hasObserver , что он и делает.


Опять же, мы используем простейшее решение, которое может пройти текущий тест:

1
2
3
4
5
function hasObserver(observer) {
  return true;
}
 
Observable.prototype.hasObserver = hasObserver;

Несмотря на то, что мы знаем, что это не решит наши проблемы в долгосрочной перспективе, тесты остаются зелеными. Попытка анализа и рефакторинга оставляет нас с пустыми руками, так как нет очевидных моментов, где мы можем улучшить. Тесты являются нашими требованиями, и в настоящее время они требуют, чтобы hasObserver возвращал значение true. Чтобы исправить это, мы введем еще один тест, который ожидает, что hasObserver return false для несуществующего наблюдателя, что может помочь форсировать реальное решение.

1
2
3
4
5
«test should return false when no observers»: function () {
  var observable = new tddjs.Observable();
 
  assertFalse(observable.hasObserver(function () {}));
}

Этот тест с треском проваливается, учитывая, что hasObserver всегда returns true, заставляя нас создавать реальную реализацию. Проверить, зарегистрирован ли наблюдатель, — это просто проверить, содержит ли массив this.observers объект, изначально переданный в addObserver :

1
2
3
function hasObserver(observer) {
  return this.observers.indexOf(observer) >= 0;
}

Метод Array.prototype.indexOf возвращает число меньше 0 если элемент отсутствует в array , поэтому проверка того, что он возвращает число, равное или большее 0 , сообщит нам, существует ли наблюдатель.


Запуск теста в более чем одном браузере дает несколько неожиданные результаты:

01
02
03
04
05
06
07
08
09
10
chris@laptop:~/projects/observable$ jstestdriver —tests all
…E
Total 4 tests (Passed: 3; Fails: 0; Errors: 1) (11.00 ms)
  Firefox 3.6.12 Linux: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (2.00 ms)
  Microsoft Internet Explorer 6.0 Windows: Run 2 tests \
(Passed: 1; Fails: 0; Errors 1) (0.00 ms)
    ObservableHasObserverTest.test should return true when has observer error \
(0.00 ms): Object doesn’t support this property or method
 
Tests failed.

Internet Explorer версий 6 и 7 не прошел тест с их наиболее общими сообщениями об ошибках: « Object doesn't support this property or method". Это может указывать на любое количество проблем:

  • мы вызываем метод для объекта, который является нулевым
  • мы вызываем метод, который не существует
  • мы обращаемся к собственности, которая не существует

К счастью, TDD-процессинг крошечными шагами, мы знаем, что ошибка связана с недавно добавленным вызовом indexOf в нашем array наблюдателей. Как выясняется, IE 6 и 7 не поддерживают метод JavaScript 1.6 Array.prototype.indexOf (за что мы не можем его винить, он только недавно был стандартизирован с помощью ECMAScript 5, декабрь 2009 г. ). На данный момент у нас есть три варианта:

  • Обойти использование Array.prototype.indexOf в hasObserver, эффективно дублируя встроенные функции в поддерживаемых браузерах.
  • Реализуйте Array.prototype.indexOf для неподдерживающих браузеров. В качестве альтернативы можно реализовать вспомогательную функцию, которая обеспечивает те же функции.
  • Используйте стороннюю библиотеку, которая предоставляет либо отсутствующий метод, либо аналогичный метод.

Какой из этих подходов лучше всего подходит для решения данной проблемы, зависит от ситуации — у всех есть свои плюсы и минусы. В интересах сохранения Observable автономным, мы просто реализуем hasObserver в виде цикла вместо вызова indexOf , чтобы эффективно обойти проблему. Между прочим, это также кажется самой простой вещью, которая могла бы сработать на этом этапе. Если мы столкнемся с подобной ситуацией позже, нам бы посоветовали пересмотреть наше решение. Обновленный hasObserver выглядит следующим образом:

1
2
3
4
5
6
7
8
9
function hasObserver(observer) {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    if (this.observers[i] == observer) {
      return true;
    }
  }
 
  return false;
}

Теперь, когда полоса становится зеленой, пришло время пересмотреть наш прогресс. Сейчас у нас есть три теста, но два из них кажутся странно похожими. Первый тест, который мы написали для проверки правильности addObserver основном проверяет те же вещи, что и тест, который мы написали для проверки Refactoring . Между этими двумя тестами есть два ключевых различия: первый тест был ранее объявлен вонючим, поскольку он непосредственно обращается к массиву наблюдателей внутри наблюдаемого объекта. Первый тест добавляет двух наблюдателей, гарантируя, что они оба добавлены. Теперь мы можем объединить тесты в один, который проверяет, действительно ли добавлены все наблюдатели, добавленные к наблюдаемой:

01
02
03
04
05
06
07
08
09
10
«test should store functions»: function () {
  var observable = new tddjs.Observable();
  var observers = [function () {}, function () {}];
 
  observable.addObserver(observers[0]);
  observable.addObserver(observers[1]);
 
  assertTrue(observable.hasObserver(observers[0]));
  assertTrue(observable.hasObserver(observers[1]));
}

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


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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
TestCase(«ObservableNotifyTest», {
  «test should call all observers»: function () {
    var observable = new tddjs.Observable();
    var observer1 = function () { observer1.called = true;
    var observer2 = function () { observer2.called = true;
 
    observable.addObserver(observer1);
    observable.addObserver(observer2);
    observable.notify();
 
    assertTrue(observer1.called);
    assertTrue(observer2.called);
  }
});

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

1
2
3
4
5
6
7
function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    this.observers[i]();
  }
}
 
Observable.prototype.notify = notify;

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

01
02
03
04
05
06
07
08
09
10
11
12
«test should pass through arguments»: function () {
  var observable = new tddjs.Observable();
  var actual;
 
  observable.addObserver(function () {
    actual = arguments;
  });
 
  observable.notify(«String», 1, 32);
 
  assertEquals([«String», 1, 32], actual);
}

Тест сравнивает полученные и переданные аргументы, присваивая полученные аргументы переменной, локальной для теста. Наблюдатель, которого мы только что создали, на самом деле очень простой ручной шпион-тестер. Запуск теста подтверждает его неудачу, что неудивительно, поскольку в настоящее время мы не касаемся аргументов внутри уведомления.

Для прохождения теста мы можем использовать apply при вызове наблюдателя:

1
2
3
4
5
function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    this.observers[i].apply(this, arguments);
  }
}

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


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


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

1
2
3
4
5
6
7
«test should throw for uncallable observer»: function () {
  var observable = new tddjs.Observable();
 
  assertException(function () {
    observable.addObserver({});
  }, «TypeError»);
}

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

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

1
2
3
4
5
6
7
function addObserver(observer) {
  if (typeof observer != «function») {
    throw new TypeError(«observer is not function»);
  }
 
  this.observers.push(observer);
}

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


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

01
02
03
04
05
06
07
08
09
10
11
«test should notify all even when some fail»: function () {
  var observable = new tddjs.Observable();
  var observer1 = function () { throw new Error(«Oops»);
  var observer2 = function () { observer2.called = true;
 
  observable.addObserver(observer1);
  observable.addObserver(observer2);
  observable.notify();
 
  assertTrue(observer2.called);
}

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

1
2
3
4
5
6
7
function notify() {
  for (var i = 0, l = this.observers.length; i < l; i++) {
    try {
      this.observers[i].apply(this, arguments);
    } catch (e) {}
  }
}

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


Мы улучшили надежность модуля Observable, предоставив ему надлежащую обработку ошибок. Модуль теперь может дать гарантии работы, если он получает хороший ввод и может восстановиться, если наблюдатель не выполнит его требования. Однако в последнем добавленном нами тесте делается предположение о недокументированных особенностях наблюдаемого: предполагается, что наблюдатели вызываются в порядке их добавления. В настоящее время это решение работает, потому что мы использовали массив для реализации списка наблюдателей. Если мы решим изменить это, наши тесты могут сломаться. Поэтому нам нужно решить: следует ли нам реорганизовать тест, чтобы он не принимал порядок вызовов, или мы просто добавили тест, который ожидает порядок вызовов — тем самым задокументировав порядок вызовов как функцию? Порядок вызовов кажется разумной функцией, поэтому наш следующий тест убедится, что Observable сохраняет это поведение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
«test should call observers in the order they were added»:
function () {
  var observable = new tddjs.Observable();
  var calls = [];
  var observer1 = function () { calls.push(observer1);
  var observer2 = function () { calls.push(observer2);
  observable.addObserver(observer1);
  observable.addObserver(observer2);
 
  observable.notify();
 
  assertEquals(observer1, calls[0]);
  assertEquals(observer2, calls[1]);
}

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


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

В интересах освобождения от классической эмуляции, которую предоставляют конструкторы, рассмотрим следующие примеры, в которых предполагается, что tddjs.observable является объектом, а не конструктором:

Примечание. Метод tddjs.extend представлен в другом месте книги и просто копирует свойства из одного объекта в другой.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// Creating a single observable object
var observable = Object.create(tddjs.util.observable);
 
// Extending a single object
tddjs.extend(newspaper, tddjs.util.observable);
 
// A constructor that creates observable objects
function Newspaper() {
  /* … */
}
 
Newspaper.prototype = Object.create(tddjs.util.observable);
 
// Extending an existing prototype
tddjs.extend(Newspaper.prototype, tddjs.util.observable);

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


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

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

1
2
3
4
5
6
7
«test should not fail if no observers»: function () {
  var observable = new tddjs.Observable();
 
  assertNoException(function () {
    observable.notify();
  });
}

С этим тестом мы можем очистить конструктор:

1
2
function Observable() {
}

Выполнение тестов показывает, что все, кроме одного, теперь не работают, все с одним и тем же сообщением: «this.observers не определен». Мы будем иметь дело с одним методом за один раз. Сначала addObserver метод addObserver :

function addObserver(observer) {
if (!this.observers) {
this.observers = [];
}

/ * … * /
}

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

1
2
3
4
5
6
7
function hasObserver(observer) {
  if (!this.observers) {
    return false;
  }
 
  /* … */
}

Мы можем применить точно такое же исправление, чтобы уведомить:

1
2
3
4
5
6
7
function notify(observer) {
  if (!this.observers) {
    return;
  }
 
  /* … */
}

Теперь, когда constructor ничего не делает, его можно безопасно удалить. Затем мы добавим все методы непосредственно в object tddjs.observable , который затем можно использовать, например, с Object.create или tddjs.extend для создания наблюдаемых объектов. Обратите внимание, что имя больше не пишется с большой буквы, так как оно больше не является конструктором. Обновленная реализация выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
(function () {
  function addObserver(observer) {
    /* … */
  }
 
  function hasObserver(observer) {
    /* … */
  }
 
  function notify() {
    /* … */
  }
 
  tddjs.observable = {
    addObserver: addObserver,
    hasObserver: hasObserver,
    notify: notify
  };
}());

Конечно, удаление конструктора приводит к тому, что все тесты до сих пор ломаются. Однако исправить их легко. Все, что нам нужно сделать, это заменить новый оператор вызовом Object.create . Однако большинство браузеров пока не поддерживают Object.create , поэтому мы можем его Object.create . Поскольку метод не может быть полностью эмулирован, мы предоставим нашу собственную версию tddjs object tddjs :

01
02
03
04
05
06
07
08
09
10
(function () {
  function F() {}
 
  tddjs.create = function (object) {
    F.prototype = object;
    return new F();
  };
 
  /* Observable implementation goes here … */
}());

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
TestCase(«ObservableAddObserverTest», {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },
 
  «test should store functions»: function () {
    var observers = [function () {}, function () {}];
 
    this.observable.addObserver(observers[0]);
    this.observable.addObserver(observers[1]);
 
    assertTrue(this.observable.hasObserver(observers[0]));
    assertTrue(this.observable.hasObserver(observers[1]));
  }
});
 
TestCase(«ObservableHasObserverTest», {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },
 
  «test should return false when no observers»: function () {
    assertFalse(this.observable.hasObserver(function () {}));
  }
});
 
TestCase(«ObservableNotifyTest», {
  setUp: function () {
    this.observable = tddjs.create(tddjs.observable);
  },
 
  «test should call all observers»: function () {
    var observer1 = function () { observer1.called = true;
    var observer2 = function () { observer2.called = true;
 
    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);
    this.observable.notify();
 
    assertTrue(observer1.called);
    assertTrue(observer2.called);
  },
 
  «test should pass through arguments»: function () {
    var actual;
 
    this.observable.addObserver(function () {
      actual = arguments;
    });
 
    this.observable.notify(«String», 1, 32);
 
    assertEquals([«String», 1, 32], actual);
  },
 
  «test should throw for uncallable observer»: function () {
    var observable = this.observable;
 
    assertException(function () {
      observable.addObserver({});
    }, «TypeError»);
  },
 
  «test should notify all even when some fail»: function () {
    var observer1 = function () { throw new Error(«Oops»);
    var observer2 = function () { observer2.called = true;
 
    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);
    this.observable.notify();
 
    assertTrue(observer2.called);
  },
 
  «test should call observers in the order they were added»:
  function () {
    var calls = [];
    var observer1 = function () { calls.push(observer1);
    var observer2 = function () { calls.push(observer2);
    this.observable.addObserver(observer1);
    this.observable.addObserver(observer2);
 
    this.observable.notify();
 
    assertEquals(observer1, calls[0]);
    assertEquals(observer2, calls[1]);
  },
 
  «test should not fail if no observers»: function () {
    var observable = this.observable;
 
    assertNoException(function () {
      observable.notify();
    });
  }
});

Чтобы избежать дублирования вызова tddjs.create , каждый тестовый пример получил method setUp который устанавливает наблюдаемое для тестирования. Методы тестирования должны быть соответствующим образом обновлены, заменив observable на this.observable.


Разработка через JavaScript на тестовой основе
В этом отрывке из книги мы подробно рассказали о разработке через тестирование с использованием JavaScript. Конечно, API в настоящее время ограничен в своих возможностях, но книга расширяет его, позволяя наблюдателям наблюдать и уведомлять пользовательские события, такие как observable.observe( » beforeLoad «, myObserver ).

Книга также дает представление о том, как вы можете применять TDD для разработки кода, который, например, в значительной степени опирается на манипуляции с DOM и Ajax, и, наконец, объединяет все примеры проектов в полнофункциональном браузерном приложении чата.

Этот отрывок основан на книге « Разработка через JavaScript на основе тестирования », автором которой является таблицу Содержание .