Deep copy and shallow copy in Node.js

今天發生一件事,原本程式碼長這樣:

build_string.js

 var facebook = require('../es_string/fb_cmt');
 var body = facebook.body;
 ...
 body.b.append({ 'query': 'A3', 'type': 'phrase'});

其中 ../es_string/fb_cmt 內長類似這樣:

exports.body = { b: [''] };

我發現當我執行兩次 build_string.js 的後,body 輸出的結果會是:

{ b: ['', { 'query': 'A3', 'type': 'phrase'}, { 'query': 'A3', 'type': 'phrase'}]}

很奇怪重複了兩次,我判斷是第一次結束後物件被保存下來,第二次的 require 並不是重新建立一個新物件,而是改寫原本參考的物件(shallow copy),所以上網找到這篇討論 node.js 中物件拷貝方法的討論

因為我需要的是每次執行 build_string.js 產生新的物件,也就是要 deep copy 物件的記憶體,所以我選擇依照網友的答案將 var body = facebook.body 那一行改寫成 var body = JSON.parse(JSON.stringify(facebook.body));,果然重新執行 build_string.js 後每次的 body 內容就沒有重複了

MySQL 簡單查詢範例

node.js

   var mysql = require('mysql');
   var connection = mysql.createConnection({
     host: '127.0.0.1',
     port: '3306',
     user: 'root',
     password: 'root',
     database: 'nodejs'
   });

  connection.connect();

  connection.query('SELECT * FROM `test`', function(err, rows, fields){
    if(err) throw err;
    for(var r in rows){
      console.log(rows[r].id);
    }
  });

 connection.end();

在 express 的 router 中這樣用:

routers/index.js

router.get('/', function(req, res){
    connection.query('SELECT * FROM `test`', function(err, rows, fields){
      res.render('index', {title: rows[0].id});
  });
});

[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] 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

[JavaScript] Test Suite 2 – Test Cases Library – Chai

Install

  1. npm install chai, bundle with package
  2. 官網補充:dependency and mocha

Intro

在 mocha 中,我們有這樣一段程式:

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

其中 node 本身雖然也提供 assert,但是 chai 專門針對這部份提供更多 API,以下是官網的說明:

The assert style is exposed through assert interface. 
This provides the classic assert-dot notation, similiar to that packaged with node.js. 
This assert module, however, provides several additional tests and is browser compatible.

Usage

Assert

 var assert = require('chai').assert
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

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

     it("chai's 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');
     })
  });

(因為是不同的 test case,所以一個 it 只會執行一種 assert,因此才要分開不同 it)

存檔成 test.js 後就可以用 mocha 來執行測試了:

 try chai with mocha
    ✓ typeOf 
    ✓ chai's equal 
    1) lengthOf
    ✓ lengthOf in array 


  3 passing (9ms)
  1 failing

  1) try chai with mocha lengthOf:
     AssertionError: foo`s value has a length of 3: expected 'bar' to have a length of 4 but got 3
      at Function.assert.lengthOf (/Users/veck/Desktop/JSTest/2_mocha_chai/node_modules/chai/lib/chai/interface/assert.js:890:37)
      at Context.<anonymous> (/Users/veck/Desktop/JSTest/2_mocha_chai/test.js:15:14)
      at callFn (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runnable.js:251:21)
      at Test.Runnable.run (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runnable.js:244:7)
      at Runner.runTest (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:374:10)
      at /Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:452:12
      at next (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:299:14)
      at /Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:309:7
      at next (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:248:23)
      at Object._onImmediate (/Users/veck/.nvm/v0.10.35/lib/node_modules/mocha/lib/runner.js:276:5)
      at processImmediate [as _immediateCallback] (timers.js:354:15)

Expect

BDD(behavior driven development) 的開發模式產生了兩種常用的測試方法:expectshould

(以下省略 mocha 部分程式碼)

var expect = require('chai').expect
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(beverages).to.have.property('tea').with.length(3);

Should

shouldexpect 基本上一樣,差別在於方法的實作上,expect 是做成 global function,而 should 是 class property,也就是物件可以用 property 的方式呼叫 expect 的測試案例

還有一點就是,expect 引用是用 require('chai').expect,但 should 是用 require('chai').should()

NOTICE: This style has some issues when used Internet Explorer, so be aware of browser compatibility.

var should = require('chai').should() //actually call the the function
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
beverages.should.have.property('tea').with.length(3);

Plugin

Chai 可以透過下面的方法來取用外部工具

chai.use(_chai, util){
    // your helpers here
}