Статьи

Ember.js Тестирование

Когда я начал играть с Ember.js почти год назад, история с тестируемостью оставляла желать лучшего. Вы можете выполнить модульное тестирование объекта без каких-либо проблем, но модульное тестирование — это только один из способов получить обратную связь при создании программного продукта. В дополнение к модульным тестам мне нужен был способ проверки интеграции нескольких компонентов. Поэтому, как и большинство людей, тестирующих богатые JavaScript-приложения, я выбрал мать всех инструментов тестирования, Selenium .

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

Но со временем небольшой, казалось бы, набор тестов, построенный на Selenium, может начать перетягивать скорость вашей команды до уровня улитки. Один из простых способов уменьшить эту боль — это избежать создания большого приложения. Если вместо этого вы создадите несколько небольших веб-приложений, это может помочь вам удержаться на плаву еще немного, потому что ни одна отдельная сборка не сокрушит команду по мере вашего роста.

Но даже в небольшом проекте реальная проблема с Selenium заключается в том, что он не является частью процесса разработки, управляемого тестами . Когда я делаю красный / зеленый / рефакторинг, у меня нет времени для медленной обратной связи в любой форме. Мне нужен был способ написания как модульных, так и интеграционных тестов, которые обеспечили бы быструю обратную связь, чтобы помочь мне сформировать программное обеспечение, которое я писал, более итеративным способом. Если вы используете версию Ember.js> = RC3, вам повезло, потому что написание модульного или интеграционного теста — это обходная часть.


Теперь, когда мы можем написать тесты JavaScript для нашего приложения, как мы их выполняем? Большинство разработчиков начинают использовать браузер напрямую, но из-за того, что я хотел что-то, что я мог выполнить без командной строки из командной строки в среде CI с богатой экосистемой, полной плагинов, я обратил внимание на Karma .

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

Но прежде чем мы сможем настроить Karma, нам нужно установить его, используя npm. Я рекомендую установить его локально, чтобы вы могли сохранять свои модули npm изолированными для каждого проекта. Для этого добавьте файл с именем package.json ‘в корень вашего проекта, который выглядит примерно так, как показано ниже.

1
2
3
4
5
6
{
  «dependencies»: {
    «karma-qunit»: «*»,
    «karma»: «0.10.2»
  }
}

Этот пример потребует как карму, так и плагин для QUnit . После сохранения вышеуказанного файла package.json вернитесь в командную строку и введите npm install чтобы развернуть необходимые модули Node .

После завершения установки npm вы увидите новую папку с именем node_modules в корне вашего проекта. Эта папка содержит весь код JavaScript, который мы только что выполнили с помощью npm, включая Karma и плагин QUnit. Если вы node_modules/karma/bin/ еще дальше в node_modules/karma/bin/ вы увидите исполняемый файл Karma. Мы будем использовать это для настройки тестового прогона, выполнения тестов из командной строки и т. Д.


Далее нам нужно настроить карму, чтобы она знала, как выполнять тесты QUnit. Наберите karma init из корня проекта. Вам будет предложено список вопросов. Первый спросит, какой тестовый фреймворк вы хотите использовать, qunit Tab, пока не увидите qunit , затем нажмите Enter . Затем ответьте no на вопрос Require.js, так как мы не будем использовать его для этого примера приложения. Нажимайте вкладку, пока не увидите PhantomJS для третьего вопроса, и вам нужно будет дважды нажать Enter, так как здесь доступно несколько вариантов. В остальном просто оставьте их по умолчанию.

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

Если вы хотите следовать, удалите сгенерированный файл конфигурации и замените его на этот.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = function(karma) {
  karma.set({
    basePath: ‘js’,
      
    files: [
      «vendor/jquery/jquery.min.js»,
      «vendor/handlebars/handlebars.js»,
      «vendor/ember/ember.js»,
      «vendor/jquery-mockjax/jquery.mockjax.js»,
      «app.js»,
      «tests/*.js»
    ],
      
    logLevel: karma.LOG_ERROR,
    browsers: [‘PhantomJS’],
    singleRun: true,
    autoWatch: false,
       
    frameworks: [«qunit»]
  });
};

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

Вверху файла конфигурации вы увидите, что я установил для basePath значение js поскольку все ресурсы JavaScript находятся в этой папке в проекте. Затем я рассказал Karma, где можно найти файлы JavaScript, необходимые для тестирования нашего простого приложения. Это включает в себя jQuery, Handlebars, Ember.js и app.js файл app.js


Теперь мы можем добавить первый файл модульного теста в проект. Сначала создайте новую папку с именем tests и вложите ее в папку js . Добавьте в этот новый каталог файл с именем unit_tests.js который будет выглядеть примерно так.

1
2
3
test(‘hello world’, function() {
  equal(1, 1, «»);
});

Этот тест пока ничего не дает, но он поможет нам убедиться, что у нас есть все, что связано с Кармой, чтобы выполнить его правильно. Обратите внимание, что в разделе files Karma мы уже добавили каталог js/tests . Таким образом, Karma будет использовать каждый файл JavaScript, который мы используем для тестирования нашего приложения.

Теперь, когда мы настроили Karma правильно, выполните тесты qunit из командной строки, используя ./node_modules/karma/bin/karma start .

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

1
2
3
test(‘hello world’, function() {
  equal(1, 2, «boom»);
});

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


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

https://dl.dropboxusercontent.com/u/716525/content/images/2013/pre-tuts.png

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

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


Модульное тестирование объекта ember легко, вы просто создаете новый экземпляр объекта и запрашиваете значение fullName .

1
2
3
4
5
test(‘fullName property returns both first and last’, function() {
  var person = App.Person.create({firstName: ‘toran’, lastName: ‘billups’});
  var result = person.get(‘fullName’);
  equal(result, ‘toran billups’, «fullName was » + result);
});

Затем, если вы вернетесь в командную строку и запустите ./node_modules/karma/bin/karma start , он должен показать один провальный тест с полезным сообщением, описывающим fullName как неопределенное в настоящее время. Чтобы это исправить, нам нужно открыть файл app.js и добавить в модель вычисляемое свойство, которое возвращает строку объединенных значений имени и фамилии.

1
2
3
4
5
6
7
8
9
App.Person = Ember.Object.extend({
  firstName: »,
  lastName: »,
  fullName: function() {
    var firstName = this.get(‘firstName’);
    var lastName = this.get(‘lastName’);
    return firstName + ‘ ‘ + lastName;
  }.property()
});

Если вы вернетесь в командную строку и запустите ./node_modules/karma/bin/karma start вы должны увидеть прохождение модульного теста. Вы можете расширить этот пример, написав несколько других модульных тестов, чтобы показать, что вычисляемое свойство должно измениться при обновлении имени или фамилии в модели.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
test(‘fullName property returns both first and last’, function() {
  var person = App.Person.create({firstName: ‘toran’, lastName: ‘billups’});
  var result = person.get(‘fullName’);
  equal(result, ‘toran billups’, «fullName was » + result);
});
 
test(‘fullName property updates when firstName is changed’, function() {
  var person = App.Person.create({firstName: ‘toran’, lastName: ‘billups’});
  var result = person.get(‘fullName’);
  equal(result, ‘toran billups’, «fullName was » + result);
  person.set(‘firstName’, ‘wat’);
  result = person.get(‘fullName’);
  equal(result, ‘wat billups’, «fullName was » + result);
});
 
test(‘fullName property updates when lastName is changed’, function() {
  var person = App.Person.create({firstName: ‘toran’, lastName: ‘billups’});
  var result = person.get(‘fullName’);
  equal(result, ‘toran billups’, «fullName was » + result);
  person.set(‘lastName’, ‘tbozz’);
  result = person.get(‘fullName’);
  equal(result, ‘toran tbozz’, «fullName was » + result);
});

Если вы добавите эти два дополнительных теста и запустите все три из командной строки, у вас должно получиться два неудачных теста. Чтобы пройти все три теста, измените свойство computed, чтобы прослушивать изменения как имени, так и фамилии. Теперь, если вы запустите ./node_modules/karma/bin/karma start из командной строки, у вас должно быть три проходных теста.

1
2
3
4
5
6
7
8
9
App.Person = Ember.Object.extend({
  firstName: »,
  lastName: »,
  fullName: function() {
    var firstName = this.get(‘firstName’);
    var lastName = this.get(‘lastName’);
    return firstName + ‘ ‘ + lastName;
  }.property(‘firstName’, ‘lastName’)
});

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

Сначала откройте файл package.json и добавьте зависимость karma-ember-preprocessor . После того, как вы обновите файл package.json , выполните команду npm install из командной строки.

1
2
3
4
5
6
7
{
  «dependencies»: {
    «karma-ember-preprocessor»: «*»,
    «karma-qunit»: «*»,
    «karma»: «0.10.2»
  }
}

Теперь, когда у вас установлен препроцессор, нам нужно проинформировать Karma о файлах шаблонов. В разделе files вашего файла karma.conf.js добавьте следующее, чтобы сообщить Karma о шаблонах Handlebars.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = function(karma) {
  karma.set({
    basePath: ‘js’,
     
    files: [
      «vendor/jquery/jquery.min.js»,
      «vendor/handlebars/handlebars.js»,
      «vendor/ember/ember.js»,
      «vendor/jquery-mockjax/jquery.mockjax.js»,
      «app.js»,
      «tests/*.js»,
      «templates/*.handlebars»
    ],
     
    logLevel: karma.LOG_ERROR,
    browsers: [‘PhantomJS’],
    singleRun: true,
    autoWatch: false,
      
    frameworks: [«qunit»]
  });
};

Далее нам нужно сообщить Karma, что делать с этими файлами handlebars, потому что технически мы хотим предварительно скомпилировать каждый шаблон перед его передачей в PhantomJS. Добавьте конфигурацию препроцессора и укажите что-либо с расширением файла *.handlebars на препроцессор ember. Также вам нужно добавить конфигурацию плагинов для регистрации препроцессора ember (наряду с несколькими другими, которые обычно включаются в конфигурацию Karma по умолчанию).

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
module.exports = function(karma) {
  karma.set({
    basePath: ‘js’,
      
    files: [
      «vendor/jquery/jquery.min.js»,
      «vendor/handlebars/handlebars.js»,
      «vendor/ember/ember.js»,
      «vendor/jquery-mockjax/jquery.mockjax.js»,
      «app.js»,
      «tests/*.js»,
      «templates/*.handlebars»
    ],
      
    logLevel: karma.LOG_ERROR,
    browsers: [‘PhantomJS’],
    singleRun: true,
    autoWatch: false,
      
    frameworks: [«qunit»],
      
    plugins: [
      ‘karma-qunit’,
      ‘karma-chrome-launcher’,
      ‘karma-ember-preprocessor’,
      ‘karma-phantomjs-launcher’
    ],
      
    preprocessors: {
      «**/*.handlebars»: ’ember’
    }
  });
};

Теперь, когда у нас есть настройка конфигурации Karma для интеграционного тестирования, добавьте новый файл с именем integration_tests.js в папку tests . Внутри этой папки нам нужно добавить простой тест, чтобы доказать, что мы можем без проблем поднять все приложение Ember.js. Добавьте простой тест qunit, чтобы увидеть, сможем ли мы перейти по маршруту '/' и получить базовый HTML-код. Для начального теста мы только утверждаем, что тег HTML существует в сгенерированном HTML.

1
2
3
4
5
6
test(‘hello world’, function() {
  App.reset();
  visit(«/»).then(function() {
    ok(exists(«table»));
  });
});

Обратите внимание, что мы используем несколько помощников, встроенных в тестирование на угасание, таких как visit и find . Помощник по visit — это удобный способ сообщить приложению, в каком состоянии находится во время выполнения. Этот тест начинается с маршрута '/' потому что именно там модели People связываются с шаблоном и генерируется наша таблица HTML. Помощник по find — это быстрый способ поиска элементов в DOM с использованием селекторов CSS, как вы бы сделали с jQuery, чтобы проверить что-то о разметке.

Прежде чем мы сможем запустить этот тест, нам нужно добавить вспомогательный файл теста, который внедрит помощников теста и установит общий корневой элемент. Добавьте приведенный ниже код в файл с integration_test_helper.js в том же каталоге tests . Это обеспечит наличие в нашем приложении помощников по тестированию во время выполнения.

1
2
3
4
5
6
7
8
9
document.write(‘<div id=»ember-testing-container»><div id=»ember-testing»></div></div>’);
  
App.rootElement = ‘#ember-testing’;
App.setupForTesting();
App.injectTestHelpers();
 
function exists(selector) {
  return !!find(selector).length;
}

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

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

1
2
3
4
5
6
7
test(‘hello world’, function() {
  App.reset();
  visit(«/»).then(function() {
    var rows = find(«table tr»).length;
    equal(rows, 2, rows);
  });
});

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

01
02
03
04
05
06
07
08
09
10
App.Person.reopenClass({
  people: [],
  find: function() {
    var first = App.Person.create({firstName: ‘x’, lastName: ‘y’});
    var last = App.Person.create({firstName: ‘x’, lastName: ‘y’});
    this.people.pushObject(first);
    this.people.pushObject(last);
    return this.people;
  }
});

Если мы сейчас запустим тесты, у нас все равно будет все, потому что два человека возвращаются, как мы и ожидали. Далее нам нужно получить ячейку таблицы, которая показывает имя человека, и утверждать, что она использует свойство fullName вместо только firstName .

1
2
3
4
5
6
7
8
9
test(‘hello world’, function() {
  App.reset();
  visit(«/»).then(function() {
    var rows = find(«table tr»).length;
    equal(rows, 2, rows);
    var fullName = find(«table tr:eq(0) td:eq(0)»).text();
    equal(fullName, «xy», «the first table row had fullName: » + fullName);
  });
});

Если вы запустите описанный выше тест, вы увидите провальный тест, потому что мы еще не обновили шаблон для использования fullName . Теперь, когда у нас есть провальный тест, обновите шаблон для использования fullName и запустите тесты, используя ./node_modules/karma/bin/karma start . Теперь у вас должен быть проходной набор тестов для модулей и интеграционных тестов.


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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
test(‘add will append another person to the html table’, function() {
  App.Person.people = [];
  App.reset();
  visit(«/»).then(function() {
    var rows = find(«table tr»).length
    equal(rows, 2, «the table had » + rows + » rows»);
    fillIn(«.firstName», «foo»);
    fillIn(«.lastName», «bar»);
    return click(«.submit»);
  }).then(function() {
    equal(find(«table tr»).length, 3, «the table of people was not complete»);
    equal(find(«table tr:eq(2) td:eq(0)»).text(), «foo bar», «the fullName for the person was incorrect»);
  });
});

Начните с проверки, с каким состоянием вы хотите работать, затем с fillIn помощника fillIn добавьте имя и фамилию. Теперь, если вы нажмете кнопку отправки , он должен добавить этого человека в таблицу HTML, поэтому при возврате мы можем утверждать, что в таблице HTML есть три человека. Запустите этот тест, и он не будет выполнен, потому что контроллер Ember не завершен.

Чтобы пройти тестирование, добавьте следующую строку в PeopleController

01
02
03
04
05
06
07
08
09
10
11
App.PeopleController = Ember.ArrayController.extend({
  actions: {
    addPerson: function() {
      var person = {
        firstName: this.get(‘firstName’),
        lastName: this.get(‘lastName’)
      };
      App.Person.add(person);
    }
  }
});

Теперь, если вы запускаете тесты, используя ./node_modules/karma/bin/karma start в отображаемом HTML должны отображаться три человека.

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

01
02
03
04
05
06
07
08
09
10
11
test(‘delete will remove the person for a given row’, function() {
  App.Person.people = [];
  App.reset();
  visit(«/»).then(function() {
    var rows = find(«table tr»).length;
    equal(rows, 2, «the table had » + rows + » rows»);
    return click(«table .delete:first»);
  }).then(function() {
    equal(find(«table tr»).length, 1, «the table of people was not complete
  });
});»)})})

Чтобы получить эту передачу, просто добавьте следующую строку в PeopleController :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
App.PeopleController = Ember.ArrayController.extend({
  actions: {
    addPerson: function() {
      var person = {
        firstName: this.get(‘firstName’),
        lastName: this.get(‘lastName’)
      };
      App.Person.add(person);
    },
    deletePerson: function(person) {
      App.Person.remove(person);
    }
  }
});

Запустите тесты из командной строки, и у вас снова будет проходящий набор тестов.


Таким образом, это завершает наш пример приложения. Не стесняйтесь задавать любые вопросы в комментариях.

Если вы предпочитаете использовать Grunt вместо karma-ember-preprocessor, просто удалите конфигурацию плагинов и препроцессоров. Также удалите templates/*.handlebars из раздела файлов, так как Karma не нужно будет предварительно скомпилировать шаблоны. Вот упрощенный karma.conf.js который работает при использовании grunt для предварительной компиляции шаблонов руля.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
module.exports = function(karma) {
  karma.set({
    basePath: ‘js’,
  
    files: [
      «lib/deps.min.js», //built by your grunt task
      «tests/*.js»
    ],
     
    logLevel: karma.LOG_ERROR,
    browsers: [‘PhantomJS’],
    singleRun: true,
    autoWatch: false,
      
    frameworks: [«qunit»]
  });
};

Вот и все!