HTML5 data-*

這是 HTML5 新加入的屬性,讓你可以自己定義要存放在 element 中的資料,而不需要干擾 class, id, type, name 這些屬性的設置

例如:

== 原本 ==

<button id="good-1" onclick="getId(this);">
<button id="good-2" onclick="getId(this);">

<script>
  function getId(good){
    var id = good.id.split('-')[1];
  }
</script>

== 改寫 ==

<button id="good-1" data-id="1" onclick="getId(this);">
<button id="good-2" data-id="2" onclick="getId(this);">

<script>
  function getId(good){
    // native js
    good.getAttribute('data-id');

    // jQeury
    good.attr('good-id');
    good.data('id')    
  }
</script>

同一個 element 的 data-* 會被存放在一個叫做 dataset 的 object 中,我們可以透過 JavaScript 呼叫 element 的 getAttribute() 來存取對應的資料內容:good.getAttribute('data-id'),也可以呼叫此 element 的 dataset:good.dataset

为什么我们要用 data-* 呢,一个最大的好处是我们可以把所有自定义属性在 dataset 对象中统一管理,遍历啊神马的都哦很方便,而不至于零零散散了,所以用用还是不错的。

Reference:

  1. http://muki.tw/tech/html5-jquery-access-data/
  2. https://www.renfei.org/blog/html5-introduction-2-data-attribute.html

[JavaScript] nodemailer

這是一個簡單易用的 Node.js 寄信模組,因為太容易使用,本文只是一個使用紀錄,官網的範例已足以滿足大家~

nodemailer

安裝套件

npm install nodemailer

寫程式

var nodemailer = require('nodemailer');

// 這個模組是透過 STMP 協定來做信件傳輸,所以需要設定代理伺服器
var transporter = nodemailer.createTransport({        // 建立 sender 物件: transporter
    service: 'Gmail',
    auth: {
        user: 'fbukevin@gmail.com',
        pass: '*******'
    }
});

// 設定 mail 選項
var mailOptions = {
    from: 'fbukevin@plsm.nccu.edu.tw',
    to: 'fbukevin@gmail.com', // 若有多個收件者,請在 ''  中用 ',' 點隔開
    subject: 'Test nodemailer',
    text: 'hello world',
    html: '<b>What does this option use?</b>' // 我一開始不知道這個是做什麼的
};

// 傳送
transporter.sendMail(mailOptions, function(error, info){
    if(error){
        console.log(error);
    }else{
        console.log('Message sent: ' + info.response);
    }
});

傳送成功的回傳訊息

fbukevin@plsm:~/test/nodemailer$ node test.js
Message sent: 250 2.0.0 OK 1441955541 sl7sm343448pbc.54 - gsmtp

[JavaScript] Rollback Testing with Sinon

mocha, chai 和 sinon,我覺得最難的是 sinon 了,因為 mocha 只是驅動,chai 是 assert ,你給他結果值就好,sinon 考慮的是物件的『行為』,最麻煩的就是如果呼叫一個函數或建立一個物件,結果會有副作用(開檔、寫檔、修改資料庫、傳送網路資料等),又不是像 try…catch… 會幫你 rollback 的話,你就得自己來處理,因此本篇想測試 sinon 的物件是否提供 rollback 或類似的測試方法。

複習一下概念:

  • spy 是 監視,所以還是會 invoke function
  • stub 是 隔離,不 invoke function,把 function 替換成 stub 物件或自定義物件函式
  • mock 是直接模擬物件或函式,包含 spy 和 stub 的所有功能,而且還可以最後來驗證物件是否照預期執行

這樣看來主要嘗試就是 spy 和 stub 物件了,mock 物件繼承上述兩者的能力,就不需去嘗試

我們拿 node.js 的 fs.openSync(); 來做例子,它的功能是同步的建立一個檔案

var sinon = require('sinon');
var assert =require('assert');
var fs = require('fs');

/**
 * test if fs.openSync() will rollback(create then delete file after test)
 * automatically with the two main kind of objects in Sinon.
 */

//Spy
describe('Spy', function(){
    it('should call open with w mode to create file', function(){
       var spy = sinon.spy(fs, "openSync");
       spy.withArgs('index.html', 'w');   // register arguments
       fs.openSync('index.html', 'w');  // call
       assert(spy.withArgs('index.html').calledOnce);
       spy.restore();
    });
});

//Stub
describe('Stub', function(){
    it('should call open with w mode to create file', function(){
        var stub = sinon.stub(fs, "openSync");
        stub.withArgs('index2.html', 'w');
        stub("index2.html", "w");
        stub.restore();
    });
});

本來還在想,如果兩者都不會 rollback,就要自己在 mocha 的 afterAll 去 remove file 了,但這樣的話 write 怎半?真的寫了怎麼再去 afterAll 做 rollback?尤其當我嘗試了 spy 並證實,很遺憾的,spy 還是會產生檔案,不會 rollback,所以不太適合寫建立和寫入的操作 …

接著邊思考邊期待,stub 好像是把指定的 funtion 替換成 stub 物件,也可以自己定義要替換成的 funtion,做到相依斬斷
也許…

rollback.png

緊張緊張,這麼重要的功能的測試的結果就要來了的啦!

Yes! stub 就沒有真的建立檔案了!
不過我認為並不是 stub 有去自動做 rollback 的動作,而是因為 stub 的功能就是要隔離相依性,亦即讓不同功能 (module, method, function) 之間戶不影響測試,因此,這個結果只是 stub 沒有去呼叫真正的那個 function 罷了!

[JavaScript] Executable node.js

  1. 建立 package.json
{
    "name": "filesearch",
      "description": "just that",
      "version": "0.1.0",
      "main": "lib/filesearch",   
      "preferGlobal": "true",
      "bin": {
             "filesearch" : "lib/filesearch.js"        // tell node where to execute 
     }
}

如果你有多個執行檔,bin 這裡可以多放幾個,名稱即指令,值為檔案路徑

  1. 建立 lib/ 和 lib/filesearhc.js
  2. filesearhc.js
#!/usr/bin/env node            // 直譯器位置(建議不要用 #!/usr/bin/node)
console.log('execute without type "node" and make it global');
  1. install our package locally
~/Desktop/exenode/lib -->npm link
/Users/veck/.nvm/v0.10.35/bin/filesearch -> /Users/veck/.nvm/v0.10.35/lib/node_modules/filesearch/lib/filesearch.js
/Users/veck/.nvm/v0.10.35/lib/node_modules/filesearch -> /Users/veck/Desktop/exenode
~/Desktop/exenode/lib -->
  1. cd to the other directory and type “filesearch" without node
~/Desktop -->filesearch 
execute without type "node" and make it global
  1. to remove installation
npm unlink -g <package-name>        // 因為我們用 link 會安裝成 global 的指令,因此要用 -g 
exe.png

如果用 npm 發佈了,只要用 npm install -g <package> 就可以安裝成 global command。

參考原文:http://javascriptplayground.com/blog/2012/08/writing-a-command-line-node-tool/

[JavaScript] Test Suit 1 – Test Framework – Mocha

Install and use

  1. npm install -g mocha 安裝完以後,就可以直接用 mocha 指定來執行測試
  2. 執行測試,mocha 會自動執行檔名為 test.js 的檔案,如果沒有會出現錯誤,你可以用 mocha <testfile> 來執行測試

Intro

Mocha 是一個 JavaScript 的 test framework,它提供用來組織 unit test 的 API,並且執行 test

Usage

var assert = require("assert")
describe('Array', function(){
  describe('#indexOf()', function(){
    it('should return -1 when the value is not present', function(){
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    })
  })
})

require

var assert = require('assert')

這個模組其實是 node.js 內建的,提供 assertion 相關 API

mocha 可以認得 describe()、it() 等函式,不需要 require

describe

describe(moduleName, testDetails)

形成一個測試區塊

moduleName 是要用到的測試模組名稱,那是測試人員隨便取的

testDetails 放置測試內容,以 callback 實作

it

it (info, function)

最基本的測試單元,通常一個 it 對應一個實際的 test case

info 是輸出訊息

function 放是這種 test assertion

Asynchronize

fs = require('fs');
describe('File', function(){
    describe('#readFile()', function(){
        it('should read test.ls without error', function(done){
            fs.readFile('test.ls', function(err){
                if (err) throw err;
                done();
            });
        })
    })
})

done() 用來指示抵達 callback 串的最深處,開始要一層層返回

这里可能会有个疑问,假如我有两个异步函数(两条分叉的回调链),那我应该在哪里加 done() 呢?实际上这个时候就不应该在一个 it 里面存在两个要测试的函数,事实上 “一个 it 里面只能调用一次 done “,当你调用多次 done 的话 mocha 会抛出错误。所以应该类似这样:(這部份是大陸網友加上去的,官網沒有提到這個,但很實用)

fs = require('fs');
describe('File', function(){
    describe('#readFile()', function(){
        it('should read test.ls without error', function(done){
            fs.readFile('test.ls', function(err){
                if (err) throw err;
                done();
            });
        })
        it('should read test.js without error', function(done){
            fs.readFile('test.js', function(err){
                if (err) throw err;
                done();
            });
        })
    })
})

像是以下的多個 assertion,因為 test case 不同,因此只有 typeOf 會被執行

describe("try chai with mocha", function(){
        assert.typeOf(foo, 'string', 'foo is a string');
        assert.equal(foo, 'bar', 'foo equal `bar`');
        assert.lengthOf(foo, 4, 'foo`s value has a length of 3');
        assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
  });

要改成這樣才會每個都執行到:

describe("try chai with mocha", function(){
     it("typeOf", function(){
        assert.typeOf(foo, 'string', 'foo is a string');
     })

     it("chai'a equal", function(){
         assert.equal(foo, 'bar', 'foo equal `bar`');
     })

     it("lengthOf", function(){
        assert.lengthOf(foo, 4, 'foo`s value has a length of 3');
     })

     it("lengthOf in array", function(){
        assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
     })
  });

Hook

describe('hooks', function() {
  before(function() {
    // 全部測試開始前就執行這裡
  })
  after(function(){
    // 全部測試結束後才執行這裡
  })
  beforeEach(function(){
    // 每個測試開始前都執行這裡
  })
  afterEach(function(){
    // 每個測試結束後都執行這裡
  })

  /* test cases */
})

Pending

就是空著 callback 函數,mocha 會 pass 這段測試,一般用在你是負責寫好框架讓別人去實作測試案例,或者測試細節還沒完全實作好(類似 mock object 的想法)

describe('Array', function(){
    describe('#indexOf()', function(){
        it('should return -1 when the value is not present', function(){
        })
    })
});

Exclusive & Inclusive

fs = require('fs');
describe('File', function(){
    describe('#readFile()', function(){
         /* skip 的 case */
        it.skip('should read test.ls without error', function(done){
            fs.readFile('test.ls', function(err){
                if (err) throw err;
                done();
            });
        })

        /* only 的 case */
        it('should read test.js without error', function(done){
        })
    })
})

這段程式碼只有 only 的會被執行,it.skip 會被忽略。每個函數中只能有一個 only,如果把 only 和 skip 一起用,因為 only 會遮蔽掉 skip 的效果,導致沒什麼效果

BDD & TDD

  • BDD: behavior driven development
  • TDD: test driven development

這兩者主要的差別就是在寫測試時的思考角度不同

mocha 預設是採用 Behavior Driven Develop (BDD),要想用 TDD 的測試需要加上參數,如:

mocha -u tdd test.js

BDD 就像上面的使用 describe() 和 it() 來構建測試程式碼,而 TDD 使用 suite() 和 test() 來組織測試

Final

基本 API 就是這樣,官網沒有過多文件,而 mocha 有些有趣的用法也請見官網

Reference

[JavaScript] Test Suit 3 – Test Double Library – Sinon

Install

  • npm install sinon
  • 相依於專案

Intro

Sinon 主要是用來作 unit test 上測試案例實體的區隔,像是 mock object 這樣的應用,也就是建立測試會用到的實體(例如裝置、呼叫到的函數),如此一來測試時就可以斬斷相依性,單獨測試某個部分

Sinon 有三大物件:spy、stub、mock,分別是 test double 中的三種替身方法:Test Spy、Test Stub、Mock Object

  • Spy:把物件或函數包起來『監視』。例如:sinon.spy(math, "power"); 監視 math.power 這個 function,然後在後面當呼叫 math.power() 後,就可以查看函數的相關訊息:
sinon.spy(math, "power");    // 監視 math.power()

...呼叫過 math.power()...

math.power.callCount > 1; // 查看函數被呼叫的次數
math.power.withArgs("xxx").calledOnce; // 該函數是否被 xxx 呼叫過一次  
math.power.restore(); // 取消監視
  • Stub:主要用來切割相依性,讓要測試的單元不受其他單元影響。以下為例,我們只是要測試 math.power 的功能,是否能正確運作(例如正確返回值),如果還要去想說 math.power 可能需要給定什麼參數才能滿足其呼叫的其他函數,就被相依性困住了,因此 stub 這樣用就可以在測試 math.power 時忽略掉 math.power 實際上會呼叫到的其他函數,或使用到的其他物件
var stub = sinon.stub();                   // 建立 stub 物件 
var stub = sinon.stub(math, "power");     // 將 math.power() 替換成為 stub 物件
var stub = sinon.stub(math, "power", function(){
                                        // 用 function 替換
                                        }); 

stub.returns(10);    // stub() 總是 return 10
stub();             // 呼叫 stub()

stub.throws("xxx"); // stub() 總是 throw "xxx"
stub(); 

stub.withArgs(1).returns(10);    // 當 stub(1) 時 return 10
stub(1); 

stub.restore();  // 用 stub.restore() 或 math.power.restore() 還原物件

在 sinon 中,stub 也具有 spy 的功能,因此可以使用 spy 的方法,例如 callCount

  • Mock:= Spy + Stub + 可以在執行測試前設定預期結果,最後檢查 mock obejct 是否有照計畫執行。如下例子中,我們一開始建立了一個 math 物件,因為不像 spy 或 stub 是用我們真正要側的 math 物件,所以是 mock 的 object
/* 建立 mock object */
var math = { ...    };
var mock = sinon.mock(math);

/* 設定 math.power(10) 預期要有 3 次 */
mock.expect("power").atLeast(3).withArgs(10); 
    ...
mock.verify();     // 檢查現在的 math 是否與其條件
mock.restore(); // 復原物件

Usage

同一測試三種版本

Spies

1 // Function under test
2 function once(fn) {
3    var returnValue, called = false;
4    return function () {
5        if (!called) {
6            called = true;
7            returnValue = fn.apply(this, arguments);
8        }
9        return returnValue;
10    };
11 }
12
13 it("calls the original function", function () {
14    var spy = sinon.spy();
15    var proxy = once(spy);
16
17    proxy();
18
19    assert(spy.called);
20 });

這樣寫的目的,就是在 line 17 先呼叫一次 once,傳入的 spy 物件在 once 中被使用,因此可以在後面 line 19 去查看 once 是否有被呼叫過(在此 spy 被使用過等效於 once 被呼叫)

Stubs

1 // Function under test
2 function once(fn) {
3    var returnValue, called = false;
4    return function () {
5        if (!called) {
6            called = true;
7            returnValue = fn.apply(this, arguments);
8        }
9        return returnValue;
10    };
11 }
12
13 it("returns the return value from the original function", function () {
14    var stub = sinon.stub().returns(42);
15    var proxy = once(stub);
16
17    assert.equals(proxy(), 42);
18 });

這樣寫就是用 stub 替換掉實際要傳入 once() 的物件,並且設定 stub 總是 return 42

Mocks

1 // Function under test
2 function once(fn) {
3    var returnValue, called = false;
4    return function () {
5        if (!called) {
6            called = true;
7            returnValue = fn.apply(this, arguments);
8        }
9        return returnValue;
10    };
11 }
12
13 it("returns the return value from the original function", function () {
14    var myAPI = { method: function () {} };
15    var mock = sinon.mock(myAPI);
16    mock.expects("method").once().returns(42);  // set expect behavior
17
18    var proxy = once(myAPI.method);        // register
19
20    assert.equals(proxy(), 42);            // invoke and test return value
21    mock.verify();                        // verify expected behavior 
22 });

在 line 14-15 我建立了一個 mock object,這個 mock object 有一個 function 叫做 method,我們設定它預期應該要以 method 為參數執行 once 並且回傳 42

跟 stub 不同的是,我們可以在最後 line 22 的地方去驗證這個 mock object 是否真的照著預期的執行

Use with Mocha and Chai

跟 mocha 和 chai 一起用的方法如下範例:

/* describe 與 it 即 mocha 方法 */
describe("mocha-chai-sino", function(){
    it("calls the original function", function () {
           var callback = sinon.spy();     // 建立 sinon.spy 物件
        var proxy = once(callback);

        proxy();

            assert(callback.called);           // 使用 assert 方法,可替換成 chai 的 api
    })
});

完整簡單測試:

  var sinon = require('sinon')
  var assert = require('assert')

  /* function to be test */
  function once(fn) { 
     var returnValue, called = false;
     return function () {
        if (!called) {
           called = true;
           returnValue = fn.apply(this, arguments);
       }
        return returnValue;
     };
  }

  /* test code */
  describe("mocha-chai-sino", function(){
     it("calls the original Functionn", function () {
        var callback = sinon.spy();// 建立 sinon.spy 物件
        var proxy = once(callback);

        proxy();

        assert(callback.called); // 使用 assert 方法,可替換成 chai 的 api
     })
  });

測試結果:

  mocha-chai-sino
    ✓ calls the original Functionn 


  1 passing (6ms)

[JavaScript] Test Suit 4 – Test Runner – Karma

Install

  1. npm install karma --save-dev (official instruction, –save-dev will register in dependency package)
  2. Karma 用 npm install -g 安裝後沒辦法直接輸入使用,要用路徑去執行,但是我改用 nvm 安裝 node.js 所以導致路徑很冗長,所以官網的安裝方式建議是直接針對你要進行測試的專案作個別安裝,這樣路徑也會比較短,我有在我的 .bashrc 中加入alias karma='./node_modules/karma/bin/karma'

Intro

karma 主要用來驅動測試,但還可以設定使用的 test framework (test runner: mocha, jasime,…etc.)、測試起始路徑 basePath(如果有設定的話,這個路徑會被加到 files list 中,然後在啟動的瀏覽器中加到 javascript 屬性 src 中作為 prefix)、設定要使用的瀏覽器、哪些檔案要被載入以便存取使用、哪些檔案只觀察變化用、哪些檔案要被排除、瀏覽器啟動時要用 port、預處理器…等等

Usage

  1. 建立 config 檔案:karma init <name>.config.js
init.png
my.config.js
// Karma configuration
// Generated on Wed Jan 07 2015 16:18:19 GMT+0800 (CST)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['mocha'],


    // list of files / patterns to load in the browser
    files: [
      'test.js'
    ],


    // list of files to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },


    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false
  });
};

2.啟動 karma 來進行測試工作:karma start <name>.config.js

karma-mocha-chai-sinon

Notice

karma 啟動瀏覽器來執行測試,會找不到 node.js 的 require(),它好像是用 RequireJS 來做為引入 module 的方法

stack-overflow
stackoverflow.png

但是 sinon 和 assert 都要用 require 來引用模組啊!儘管用了 require.js ,還是會因為載入慢導致在啟動瀏覽器後來找不到 sinon

Chrome 39.0.2171 (Mac OS X 10.10.1) ERROR
  Uncaught Error: Module name "sinon" has not been loaded yet for context: _. Use require([])
  http://requirejs.org/docs/errors.html#notloaded
  at /Users/veck/Desktop/JSTest/5/node_modules/requirejs/require.js:141

注意到 npm 上有 karma-chai 和 karma-sinon 這兩個專案,做法都是安裝 plugin 後,只要在 config.js 的 framework 填入 ‘sinon’ 和 ‘chai’,就可以直接在 test 中 不用 require 就使用 sinon 和 chai 物件

karma-sinon

先來解決使用 sinon 的問題:

  1. npm install karma-sinon
  2. 修改 .config.js 的 frameworks:frameworks: ['mocha', 'sinon']
  3. 修改 test.js,不需要 var sinon = require(‘sinon’) 了,這設計很方便的直接可以用
  4. karma start <name>.config.js !
Chrome 39.0.2171 (Mac OS X 10.10.1) mocha-chai-sino calls the original Functionn FAILED
    ReferenceError: assert is not defined
        at Context.<anonymous> (/Users/veck/Desktop/JSTest/test_karma/test.js:24:7)
Chrome 39.0.2171 (Mac OS X 10.10.1): Executed 1 of 1 (1 FAILED) ERROR (0.014 secs / 0.003 secs)

OK!解決了找不到 require 和 sinon 沒有載入的問題,剩下 assert

karma-chai

node.js 原本的 assert module 還是要 require 才能用,想起在學 chai 的時候,chai 的官網有說 chai 也有實現 traditional style assert,因此 assert 可以用 karma-chai 的 chai 來實現了

  1. npm install karma-chai
  2. 修改 .config.js 的 frameworks:frameworks: ['mocha', 'sinon', 'chai']
  3. 修改 test.js,不需要 var assert = require(‘assert’) 了
  4. karma start <name>.config.js !
  5. YA!
success.png

最後整個 test.js:

/* function under test */
function once(fn) {
   var returnValue, called = false;
   return function () {
      if (!called) {
         called = true;
         returnValue = fn.apply(this, arguments);
      }
      return returnValue;
   };
}

/* test code */
describe("karma-mocha-chai-sino", function(){
   it("calls the original Functionn", function () {
      var callback = sinon.spy();// 建立 sinon.spy 物件
      var proxy = once(callback);

      proxy();

      assert(callback.called);
   })
});

解到此時,正好休息去洗了個澡,想說既然這 sinon 和 chai 的組合很常在 JavaScript unit test 中用到,應該可以寫個 karma 和 sinon 及 chai 的 plugin,就不用安裝和設定兩次 frameworks 了,先上網查了一下,果然也有人整了一個 karma-sinon-chai,config.js 那邊要用 sinon-chai 這個 framework name

如此一來,就解決 karma 是 browser-based 的問題了!

Refernce