Upgrade to Pro — share decks privately, control downloads, hide ads and more …

BDD in Backbone.js with Jasmine and RequireJS

BDD in Backbone.js with Jasmine and RequireJS

F830ec52d5bf72ee64fd1a43a6a82a49?s=128

大澤木小鐵

October 27, 2012
Tweet

Transcript

  1. @ 2012 JS 嘉年華會 with RequireJS / Jasmine BDD in

    Backbone.js
  2. Jace Ju 大澤木小鐵 http://plurk.com/jaceju http://twitter.com/jaceju http://www.jaceju.net

  3. 快速回顧

  4. RequireJS 1

  5. Require.config require.config({ paths: { 'jquery': 'path/to/jquery', 'alias_1': 'path', 'alias_2': 'path'

    }, shim: { 'alias_2': { deps: ['jquery', 'alias_1'], exports: '$' }, 'alias_2': ['jquery', 'alias_1'] } });
  6. define define([ 'alias', 'plugin_alias!path/to/file' ], function () { // Module

    code });
  7. Backbone.js 2

  8. Model define(['backbone'], function () { return Backbone.Model.extend({ defaults: { attr_1:

    'default_value', attr_2: 'default_value' }, method_name: function() { } }); });
  9. Collection define(['model_alias', 'backbone'], function (Model) { return Backbone.Collection.extend({ model: Model

    method_name: function () { } }); });
  10. View define([ 'SubView', 'text!template/template.html', 'backbone' ], function (SubView, template_name) {

    return Backbone.View.extend({ el: '#id', template: _.template(template_name), initialize: function () { }, events: { 'event selector': 'method_name' }, method_name: function(e) { var view = new SubView(); this.$el.append(view.render().el); }, render: function () { this.$el.html(this.template(this.model.toJSON())); return this; } }); });
  11. Jasmine 3

  12. describe define(['model_alias'], function (Model) { describe('Spec description', function () {

    }); });
  13. describe define(['model_alias'], function (Model) { describe('Spec description', function () {

    var fixture; beforeEach(function () { fixture = new Model(); }); afterEach(function () { fixture = null; }); }); });
  14. describe define(['model_alias'], function (Model) { describe('Spec description', function () {

    var fixture; beforeEach(function () { fixture = new Model(); }); afterEach(function () { fixture = null; }); it('do something', function () { expect(fixture).toBe(expectation); }); }); });
  15. matcher .toBe(some_instance) .not.toBe(some_instance) .toEqual(12) .toMatch(/bar/) .toBeDefined() .toBeUndefined() .toBeNull() .toBeTruthy() .toBeFalsy()

    .toContain('bar') .toBeLessThan(13) .toThrow() ......
  16. 什麼是 BDD

  17. Behavior-Driven Development

  18. Behavior-Driven Development Boss

  19. 範例:Todo

  20. 戰略地圖 1

  21. 待辦事項 待辦事項 待辦事項 清除已完成項目 (1) 待辦事項 新增待辦事項 完成待辦事項 刪除待辦事項 清除已完成事項

  22. 作戰會議 2

  23. 看圖說故事 新增待辦事項 ⾏行為:輸⼊入待辦事項並按下 Enter 鍵 預期:資料集中新增⼀一筆資料 預期:列表新增⼀一筆待辦事項 完成待辦事項 ⾏行為:勾選⼀一筆待辦事項 預期:該待辦事項資料狀態變更為已完成

    預期:該待辦事項加上刪除線 預期:右下⽅方顯⽰示「清除已完成事項 (1) 」 刪除待辦事項 ⾏行為:按下⼀一筆待辦事項的刪除鈕 預期:刪除該待辦事項 (消失) 清除已完成事項 ⾏行為:按下「清除已完成事項」鈕 預期:將所有已完成事項清除 預期:右下⽅方隱藏「清除已完成事項」
  24. Collection / Model 的行為 畫⾯面上看不到的事 新增資料 ⾏行為:新增兩筆資料 預期:資料筆數應為 2 刪除資料

    ⾏行為:刪除第⼆二筆資料 預期:資料筆數應為 1 更新資料狀態 ⾏行為:更新第⼀一筆資料狀態 預期:第⼀一筆資料狀態應為已完成 清空資料 ⾏行為:清空已完成項⺫⽬目 預期:資料筆數應為 0
  25. 部隊集合 3

  26. 程式架構 !"" css !"" img !"" jasmine !"" libs !""

    js # !"" collections # !"" models # !"" spec # !"" templates # !"" views # !"" test-runner.js # !"" script.js # $"" app.js !"" test.html $"" index.html
  27. 不用一開始就寫齊,有缺再回來補 js/script.js require.config({ paths: { 'text': '../libs/require-text', 'jquery': '../libs/jquery', 'underscore':

    '../libs/underscore', 'backbone': '../libs/backbone', 'backbone-localstorage': '../libs/backbone-localstorage', 'jasmine': '../jasmine/jasmine', 'jasmine-jquery': '../jasmine/jasmine-jquery', 'jasmine-html': '../jasmine/jasmine-html', 'todo-spec': 'spec/todo', 'Todos': 'collections/todos', 'Todo': 'models/todo', 'MainView': 'views/main', 'TodoItemView': 'views/todo-item' },
  28. shim: { 'jquery': { exports: '$' }, 'underscore': { exports:

    '_' }, 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' }, 'backbone-localstorage': ['backbone'], 'jasmine': { exports: 'jasmine' }, 'jasmine-jquery': ['jasmine', 'jquery'], 'jasmine-html': ['jasmine'], 'todo-spec': ['jasmine-html', 'jasmine-jquery'] } }); 將 library 之間的相依性定義好 js/script.js
  29. 稍後的規格要寫在這裡 相關類別、樣版與規格程式 js !"" collections # $"" todos.js !"" models

    # $"" todo.js !"" spec # $"" todo.js !"" templates # $"" item.html $"" views !"" main.js $"" todo-item.js
  30. 執⾏行計劃 4

  31. describe('Collection/Model 測試', function () { beforeEach(function () { }); afterEach(function

    () { }); describe 區分 Model 及 View 把規格寫到測試中
  32. describe('Collection/Model 測試', function () { beforeEach(function () { }); afterEach(function

    () { }); it('確認資料筆數', function () { }); it('切換資料狀態', function () { }); it('刪除資料', function () { }); 把規格寫到測試中 大標寫到 it
  33. describe('Collection/Model 測試', function () { beforeEach(function () { }); afterEach(function

    () { }); it('確認資料筆數', function () { // 預期:資料筆數應為 2 }); it('切換資料狀態', function () { // ⾏行為:切換第⼀一筆資料狀態 // 預期:第⼀一筆資料狀態應為已完成 }); it('刪除資料', function () { // ⾏行為:刪除第⼆二筆資料 // 預期:資料筆數應為 1 }); 行為與預期結果寫到 callback 的註解 把規格寫到測試中
  34. describe('待辦事項介⾯面⾏行為', function () { beforeEach(function () { }); afterEach(function ()

    { }); it('新增待辦事項', function () { // ⾏行為:輸⼊入待辦事項並按下 Enter 鍵 // 預期:資料集中新增⼀一筆資料 // 預期:列表新增⼀一筆待辦事項 // 預期:下⽅方顯⽰示剩餘 1 筆待辦事項 }); // ... 略 ... }); 介⾯面的規格 為了不干擾測試的結果,未完成的 測試可以暫時先註解起來
  35. ⽕火⼒力試射 5

  36. 戰技指導 • 直接依照規格的⾏行為來撰寫程式碼 • 可以開啟 LiveReload 及 Firebug • 測試重要邏輯即可

    • 如何顯⽰示測試結果? Jasmine HTML Reporter 附在 Jasmine 的下載包中
  37. js/test-runner.js define([ 'todo-spec' ], function () { return { run:

    function () { var jasmineEnv = jasmine.getEnv(); var htmlReporter = new jasmine.HtmlReporter(); jasmineEnv.updateInterval = 1000; jasmineEnv.addReporter(htmlReporter); jasmineEnv.specFilter = function (spec) { return htmlReporter.specFilter(spec); }; var currentWindowOnload = window.onload; window.onload = function () { if (currentWindowOnload) { currentWindowOnload(); } jasmineEnv.execute(); }; } }}); 將 spec 載入
  38. test.html <script data-main="js/script" src="libs/require.js"></script> </head> js/script.js define(['test-runner'], function(Target) { Target.run();

    });
  39. 測試執⾏行結果

  40. 測試 Collection / Model ⾏行為 describe('Collection/Model 測試', function () {

    var collection; beforeEach(function () { // 新增兩筆資料 collection = new Todos([ { "id": 1, "title": "todo 1", "completed": false }, { "id": 2, "title": "todo 2", "completed": false } ]); }); it('確認資料筆數', function () { // 預期:資料筆數應為 2 expect(collection.size()).toEqual(2); }); // 略... 以它會有什麼行為來寫測試
  41. js/collections/todos.js define([ 'Todo', 'backbone' ], function (Todo) { return Backbone.Collection.extend({

    model: Todo }); }); 有錯誤是正常的, BDD 就是 朝正確結果去修正程式碼
  42. Model define([ 'backbone' ], function () { return Backbone.Model.extend({ defaults:

    { title: '', completed: false } }); });
  43. 測試 Model 切換狀態的⾏行為 it('⾏行為:切換第⼆二筆資料狀態', function () { // 預期:第⼆二筆資料狀態應為已完成 var

    model = collection.get(2); expect(model.get('completed')).toBeFalsy(); model.toggle(); expect(model.get('completed')).toBeTruthy(); }); 透過自訂的 toggle 方法來切換
  44. js/models/todo.js define([ 'backbone' ], function () { return Backbone.Model.extend({ defaults:

    { title: '', completed: false }, toggle: function() { this.save({ completed: !this.get('completed') }); } }); }); 完成 toggle 方法, 但是 save 需要 AJAX 支援
  45. AJAX 怎麼測試? • 模擬 AJAX https://github.com/joneath/jasmine-ajax-mock • 改⽤用 localStorage http://documentcloud.github.com/backbone/docs/backbone-localstorage.html

  46. js/collections/todos.js define([ 'Todo', 'backbone-localstorage' ], function (Todo) { return Backbone.Collection.extend({

    model: Todo, localStorage: new Store('todos-backbone'), }); }); 因為相依性已經定義好了, 所以直接呼叫 backbone-localstorage 即可
  47. 地圖推演 6

  48. 戰技指導 • 找出使⽤用者會操作到的 DOM 元素 • 找出會反應使⽤用者操作的 DOM 元素 •

    將以上兩者以 jQuery 包裝 • 加⼊入需要測試的 model / view • 介⾯面 HTML 如何載⼊入? 有用到再加上去 https://github.com/velesin/jasmine-jquery
  49. getFixtures 與 loadFixtrues 是由 jasmine-jquery 提供 可以用來載入 HTML 介面做為 Fixture

    載⼊入 HTML 介⾯面 jasmine.getFixtures().fixturesPath = './'; describe('待辦事項介⾯面動作', function() { beforeEach(function () { // Fixtures loadFixtures('index.html');
  50. 準備其他 Fixtures describe('待辦事項介⾯面動作', function() { var ENTER_KEY = 13; var

    todos = null; var main_view = null; var $new_todo = null; var $todo_list = null; beforeEach(function () { // Fixtures loadFixtures('index.html'); // DOM $new_todo = $('#new-todo'); $todo_list = $('#todo-list'); // Collection todos = new Todos(); // View main_view = new MainView({ collection: todos }); }); 建立該測試需要 用到的 fixtures 就好
  51. it('新增待辦事項', function () { // 動作:輸⼊入待辦事項並按下 Enter 鍵 var title

    = 'todo 1'; $new_todo.val(title); e = jQuery.Event("keypress"); e.which = ENTER_KEY; e.keyCode = ENTER_KEY; $new_todo.trigger(e); // 結果:資料集中新增⼀一筆資料 expect(todos.size()).toEqual(1); // 結果:列表新增⼀一筆待辦事項 expect($('label:eq(0)', $todo_list).text()).toEqual(title); }); 模擬按下 Enter 鍵 介⾯面⾏行為
  52. 主要介⾯面 define([ 'backbone' ], function () { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { }, events: { }
  53. expect(todos.size()).toEqual(1);

  54. expect(todos.size()).toEqual(1); define([ 'backbone' ], function () { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { }, events: { }
  55. expect(todos.size()).toEqual(1); define([ 'backbone' ], function () { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { this.input = this.$('#new-todo'); }, events: { 'keypress #new-todo': 'createOnEnter' }
  56. expect(todos.size()).toEqual(1); define([ 'backbone' ], function () { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { this.input = this.$('#new-todo'); }, events: { 'keypress #new-todo': 'createOnEnter' }, createOnEnter: function(e) { if (e.which !== 13 || !this.input.val().trim()) { return; } this.collection.create({ title: this.input.val().trim(), completed: false }); this.input.val(''); }
  57. expect($('label:eq(0)', $todo_list).text()).toEqual(title); define([ 'backbone' ], function ( ) { return

    Backbone.View.extend({ el: '#todo-app', initialize: function () { this.input = this.$('#new-todo'); }, // ... }); });
  58. define([ 'backbone' ], function ( ) { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { this.input = this.$('#new-todo'); this.list = this.$('#todo-list'); this.collection.on('add', this.addOne, this); }, // ... }); }); 這裡我們需要列表 並在 collection 在新增完資料後 能夠透過 callback 更新列表 expect($('label:eq(0)', $todo_list).text()).toEqual(title);
  59. define([ 'TodoItemView', 'backbone' ], function (TodoItemView) { return Backbone.View.extend({ el:

    '#todo-app', initialize: function () { this.input = this.$('#new-todo'); this.list = this.$('#todo-list'); this.collection.on('add', this.addOne, this); }, // ... addOne: function(todo) { var view = new TodoItemView({ model: todo }); this.list.append(view.render().el); } }); }); 這裡需要一個動態建立的 sub view , 然後把新增的 model 交給它顯示 expect($('label:eq(0)', $todo_list).text()).toEqual(title);
  60. define([ 'text!templates/item.html', 'backbone' ], function (item_template) { return Backbone.View.extend({ template:

    _.template(item_template), render: function () { this.$el.html(this.template(this.model.toJSON())); return this; } }); }); 基本的 View 架構 js/views/todo-item.js
  61. 實際作戰 8

  62. 在瀏覽器上實測 • 多數時候測試碼就是實際運作程式 • 可以透過 Router 來整合 M/C/V • 瀏覽器⾏行為還是會有差異

    • 如何不重複測試與正式的 require.config ?
  63. test.html <script> var target_app = 'test-runner'; </script> <script data-main="js/script" src="libs/require.js"></script>

    利用 target_app 全域變數來切換 index.html <script> var target_app = 'app'; </script> <script data-main="js/script" src="libs/require.js"></script> js/script.js define([target_app], function(Target) { Target.run(); });
  64. 援軍到達 9

  65. 神兵利器 直接用瀏覽器測試 • Selenium http://seleniumhq.org/ http://sinonjs.org/ • Sinon.JS Spies /

    Mock / Stub Framework
  66. 問題與討論