Node.js單元測試&CI CD

講者介紹

  • 李昀陞
  • Peter
  • 後端開發者
  • 技術宅
  • email: peter279k@gmail.com

在開始講單元測試之前

什麼是JavaScript?

JavaScript

  • JavaScript != JAVA
  • 一個瀏覽器上運行程式語言
  • 在網頁上活躍
  • 以ECMAScript實現語言標準化

那什麼是Node.js

Node.js

  • 不是程式語言
  • 是一個環境讓JavaScript可以運行在後端
  • 基於Google開發V8引擎
  • 跨平台

Node.js特色

  • 非阻塞式(non-blocking)
  • 事件驅動(Event-driven)

Node.js環境安裝

Windows

  • 下載連結
  • 目前Release版本:9.2.0
  • LTS版本:8.9.2

相依套件管理

NPM(Node Package Manager)

YARN

套件市集

進入正題吧!

什麼是單元測試?

在講單元測試之前…

談談測試的類型

測試類型

單元測試

  • 單位/單元=function or method
  • 最小單位功能
  • xUnit測試框架

Node.js上的單位測試

Lab 1&Lab 2

Assertions

建置Lab 1

  • 下載Git-For-Windows
  • 打開Git-bash
    • git clone https://github.com/peter279k/Calculator.js
  • 打開Node.js command prompt
    • cd Calcualtor.js
    • npm install

src/util/Calculator.js

function Calculator(num1, num2) {
    this.num1 = num1;
    this.num2 = num2;

    this.add = function(num1 = this.num1, num2 = this.num2) {
        if(isNaN(num1)) {
            throw new TypeError(num1 + ' is not the valid number');
        }

        if(isNaN(num2)) {
            throw new TypeError(num2 + ' is not the valid number');
        }

        return parseFloat(num1) + parseFloat(num2);
    };

    this.minus = function(num1 = this.num1, num2 = this.num2) {
        if(isNaN(num1)) {
            throw new TypeError(num1 + ' is not the valid number');
        }

        if(isNaN(num2)) {
            throw new TypeError(num2 + ' is not the valid number');
        }

        return parseFloat(num1) - parseFloat(num2);
    }

    this.mul = function(num1 = this.num1, num2 = this.num2) {
        if(isNaN(num1)) {
            throw new TypeError(num1 + ' is not the valid number');
        }

        if(isNaN(num2)) {
            throw new TypeError(num2 + ' is not the valid number');
        }

        return parseFloat(num1) * parseFloat(num2);
    }

    this.divide = function(num1 = this.num1, num2 = this.num2) {
        if(isNaN(num1)) {
            throw new TypeError(num1 + ' is not the valid number');
        }

        if(isNaN(num2)) {
            throw new TypeError(num2 + ' is not the valid number');
        }

        num1 = parseFloat(num1);
        num2 = parseFloat(num2);

        if(num2 == 0) {
            throw new Error(num2 + ' should not be the zero number');
        }

        return num1 / num2;
    }
}

module.exports = Calculator;

src/util/SimpleMath.js

function SimpleMath(num1, num2) {
    this.num1 = num1;
    this.num2 = num2;

    this.pow = function(num1 = this.num1, num2 = this.num2) {
        if(isNaN(num1)) {
            throw new TypeError(num1 + ' is not the valid number');
        }

        if(isNaN(num2)) {
            throw new TypeError(num2 + ' is not the valid number');
        }

        num1 = parseFloat(num1);
        num2 = parseFloat(num2);

        return Math.pow(num1, num2);
    }

    this.sqrt = function(num = this.num) {
        if(isNaN(num)) {
            throw new TypeError(num + ' is not the valid number');
        }

        num = parseFloat(num);

        return Math.sqrt(num);
    }

    this.log = function(num = this.num1) {
        if(isNaN(num)) {
            throw new TypeError(num + ' is not the valid number');
        }

        num = parseFloat(num);

        return Math.log(num);
    }

    this.logTen = function(num = this.num1) {
        if(isNaN(num)) {
            throw new TypeError(num + ' is not the valid number');
        }

        num = parseFloat(num);

        return Math.log10(num);
    }
}

module.exports = SimpleMath;

test/CalculatorTest.js

const assert = require('assert');

var calculatorjs = require('../src/main');
var Calculator = calculatorjs.Calculator;
var SimpleMath = calculatorjs.SimpleMath;
var calculator = new Calculator(1, 2);
var simpleMath = new SimpleMath(2, 3);

// assert.equal(expected, result);

assert.equal(3, calculator.add());
assert.equal(-1, calculator.minus());
assert.equal(2, calculator.mul());
assert.equal(0.5, calculator.divide());

assert.equal(8, simpleMath.pow());
assert.equal(2, simpleMath.sqrt(4));
assert.equal(0, simpleMath.log(1));
assert.equal(0, simpleMath.logTen(1));

// It's equivalent to assert.throws(function() {calculator.add('ff', 'ff');}, TypeError);

assert.throws(() => calculator.add('ff', 'ff'), TypeError);
assert.throws(() => calculator.minus('ff', 'ff'), TypeError);
assert.throws(() => calculator.mul('ff', 'ff'), TypeError);
assert.throws(() => calculator.divide('ff', 'ff'), TypeError);
assert.throws(() => calculator.divide(1, 0), Error);

npm run assert-test

Mocha

  • 補足內建Assert的不足
  • 實踐TDD(Test-Driven Development)
  • 實踐BDD(Behavior-Driven Development)

npm test

紅燈

綠燈

程式碼涵蓋率

  • Code Coverage
  • 確認程式碼是否有執行到
  • 努力到100%

程式碼涵蓋率工具

  • istanbul
  • npm run istanbul-test
  • npm run mocha-istanbul-text-test
  • npm run mocha-istanbul-html-test

npm run istanbul-test

npm run mocha-istanbul-text-test

npm run mocha-istanbul-html-test

./coverage/index.html

涵蓋率測試方法

  • Graph Coverage
  • Stress Testing Coverage
  • Logical Expression Coverage

Graph Coverage

  • Edge Coverage
  • Edge-Pair Coverage
  • Test Path Coverage

Stress Testing Coverage

  • Unexpected Test Case
  • Empty String
  • Big Number
  • Strings

Logical Expression Coverage

  • Predicate Coverage
  • Clause Coverage
  • Combinatorial Coverage

Continuous Integration

  • 持續整合
  • 每當專案有commit時啟動
  • 可以自動化做測試

Travis-CI

串接範例(.travis.yml)

sudo: false
language: node_js
node_js:
  - 6
  - 8
  - "stable"

script: npm test

after_success:
  - npm run-script coveralls-coverage
  - npm run-script codecov-coverage

將專案與Travis-CI連接

Code Coverage Service連接

Code Coverage Service連接

Lab 2內容

  • Coordinate.js
  • 判斷座標在第幾個象限

Lab 2 Live Demo

涵蓋率方法實踐

Any Questions?

Thank you for your listening!