Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

快速回顧

Slide 4

Slide 4 text

RequireJS 1

Slide 5

Slide 5 text

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'] } });

Slide 6

Slide 6 text

define define([ 'alias', 'plugin_alias!path/to/file' ], function () { // Module code });

Slide 7

Slide 7 text

Backbone.js 2

Slide 8

Slide 8 text

Model define(['backbone'], function () { return Backbone.Model.extend({ defaults: { attr_1: 'default_value', attr_2: 'default_value' }, method_name: function() { } }); });

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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; } }); });

Slide 11

Slide 11 text

Jasmine 3

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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); }); }); });

Slide 15

Slide 15 text

matcher .toBe(some_instance) .not.toBe(some_instance) .toEqual(12) .toMatch(/bar/) .toBeDefined() .toBeUndefined() .toBeNull() .toBeTruthy() .toBeFalsy() .toContain('bar') .toBeLessThan(13) .toThrow() ......

Slide 16

Slide 16 text

什麼是 BDD

Slide 17

Slide 17 text

Behavior-Driven Development

Slide 18

Slide 18 text

Behavior-Driven Development Boss

Slide 19

Slide 19 text

範例:Todo

Slide 20

Slide 20 text

戰略地圖 1

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

作戰會議 2

Slide 23

Slide 23 text

看圖說故事 新增待辦事項 ⾏行為:輸⼊入待辦事項並按下 Enter 鍵 預期:資料集中新增⼀一筆資料 預期:列表新增⼀一筆待辦事項 完成待辦事項 ⾏行為:勾選⼀一筆待辦事項 預期:該待辦事項資料狀態變更為已完成 預期:該待辦事項加上刪除線 預期:右下⽅方顯⽰示「清除已完成事項 (1) 」 刪除待辦事項 ⾏行為:按下⼀一筆待辦事項的刪除鈕 預期:刪除該待辦事項 (消失) 清除已完成事項 ⾏行為:按下「清除已完成事項」鈕 預期:將所有已完成事項清除 預期:右下⽅方隱藏「清除已完成事項」

Slide 24

Slide 24 text

Collection / Model 的行為 畫⾯面上看不到的事 新增資料 ⾏行為:新增兩筆資料 預期:資料筆數應為 2 刪除資料 ⾏行為:刪除第⼆二筆資料 預期:資料筆數應為 1 更新資料狀態 ⾏行為:更新第⼀一筆資料狀態 預期:第⼀一筆資料狀態應為已完成 清空資料 ⾏行為:清空已完成項⺫⽬目 預期:資料筆數應為 0

Slide 25

Slide 25 text

部隊集合 3

Slide 26

Slide 26 text

程式架構 !"" css !"" img !"" jasmine !"" libs !"" js # !"" collections # !"" models # !"" spec # !"" templates # !"" views # !"" test-runner.js # !"" script.js # $"" app.js !"" test.html $"" index.html

Slide 27

Slide 27 text

不用一開始就寫齊,有缺再回來補 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' },

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

稍後的規格要寫在這裡 相關類別、樣版與規格程式 js !"" collections # $"" todos.js !"" models # $"" todo.js !"" spec # $"" todo.js !"" templates # $"" item.html $"" views !"" main.js $"" todo-item.js

Slide 30

Slide 30 text

執⾏行計劃 4

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

⽕火⼒力試射 5

Slide 36

Slide 36 text

戰技指導 • 直接依照規格的⾏行為來撰寫程式碼 • 可以開啟 LiveReload 及 Firebug • 測試重要邏輯即可 • 如何顯⽰示測試結果? Jasmine HTML Reporter 附在 Jasmine 的下載包中

Slide 37

Slide 37 text

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 載入

Slide 38

Slide 38 text

test.html js/script.js define(['test-runner'], function(Target) { Target.run(); });

Slide 39

Slide 39 text

測試執⾏行結果

Slide 40

Slide 40 text

測試 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); }); // 略... 以它會有什麼行為來寫測試

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Model define([ 'backbone' ], function () { return Backbone.Model.extend({ defaults: { title: '', completed: false } }); });

Slide 43

Slide 43 text

測試 Model 切換狀態的⾏行為 it('⾏行為:切換第⼆二筆資料狀態', function () { // 預期:第⼆二筆資料狀態應為已完成 var model = collection.get(2); expect(model.get('completed')).toBeFalsy(); model.toggle(); expect(model.get('completed')).toBeTruthy(); }); 透過自訂的 toggle 方法來切換

Slide 44

Slide 44 text

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 支援

Slide 45

Slide 45 text

AJAX 怎麼測試? • 模擬 AJAX https://github.com/joneath/jasmine-ajax-mock • 改⽤用 localStorage http://documentcloud.github.com/backbone/docs/backbone-localstorage.html

Slide 46

Slide 46 text

js/collections/todos.js define([ 'Todo', 'backbone-localstorage' ], function (Todo) { return Backbone.Collection.extend({ model: Todo, localStorage: new Store('todos-backbone'), }); }); 因為相依性已經定義好了, 所以直接呼叫 backbone-localstorage 即可

Slide 47

Slide 47 text

地圖推演 6

Slide 48

Slide 48 text

戰技指導 • 找出使⽤用者會操作到的 DOM 元素 • 找出會反應使⽤用者操作的 DOM 元素 • 將以上兩者以 jQuery 包裝 • 加⼊入需要測試的 model / view • 介⾯面 HTML 如何載⼊入? 有用到再加上去 https://github.com/velesin/jasmine-jquery

Slide 49

Slide 49 text

getFixtures 與 loadFixtrues 是由 jasmine-jquery 提供 可以用來載入 HTML 介面做為 Fixture 載⼊入 HTML 介⾯面 jasmine.getFixtures().fixturesPath = './'; describe('待辦事項介⾯面動作', function() { beforeEach(function () { // Fixtures loadFixtures('index.html');

Slide 50

Slide 50 text

準備其他 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 就好

Slide 51

Slide 51 text

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 鍵 介⾯面⾏行為

Slide 52

Slide 52 text

主要介⾯面 define([ 'backbone' ], function () { return Backbone.View.extend({ el: '#todo-app', initialize: function () { }, events: { }

Slide 53

Slide 53 text

expect(todos.size()).toEqual(1);

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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' }

Slide 56

Slide 56 text

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(''); }

Slide 57

Slide 57 text

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'); }, // ... }); });

Slide 58

Slide 58 text

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);

Slide 59

Slide 59 text

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);

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

實際作戰 8

Slide 62

Slide 62 text

在瀏覽器上實測 • 多數時候測試碼就是實際運作程式 • 可以透過 Router 來整合 M/C/V • 瀏覽器⾏行為還是會有差異 • 如何不重複測試與正式的 require.config ?

Slide 63

Slide 63 text

test.html var target_app = 'test-runner'; 利用 target_app 全域變數來切換 index.html var target_app = 'app'; js/script.js define([target_app], function(Target) { Target.run(); });

Slide 64

Slide 64 text

援軍到達 9

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

問題與討論