Node js TDD開發的起手式

yellow超發表於2018-05-03

個人覺得國內的開發氛圍,對於程式碼進行單元測試是一件很奢侈的事。因為大部分人都是從大學才開始接觸程式設計的,社會中浮躁的氛圍也或多或少的帶入到了大學校園,所以老師對同學們的程式碼也就僅僅要求上機執行成功就ok了,從而下意識的給同學們帶來了一種你的程式碼只要跑通了,就完事大吉的錯誤想法,回過頭來,仔細想想,我們可能只考慮到的程式碼執行的一種走向,而N-1種情況卻絲毫沒有考慮到。這為以後工作中開發成熟的使用者產品賣下了隱患,當然我也不例外,也是受到大學氛圍的荼毒,但是幸運的是,我遇到了一個開發要求相對較高的工作環境(^__^),我們小組以TDD形式開發後端,變數命名,見名知意,eslint程式碼格式檢查,不予許寫註釋(WTF, 還有不許寫註釋的咯!其實,這個要求還挺好的,讓我們更加關注程式碼的可讀性),測試程式碼的覆蓋率等等。

廢話港了這麼多,回到主題,當你看完我的BB後,你可以做到如下幾個點:

  • 搭建Node.js的測試環境
  • http請求的的測試方式
  • 測試非同步方法
  • mock依賴外部的程式碼
  • 從所未有的對自己程式碼的自信 (key point)

Node.js 測試環境搭建

首先,package.json的配置

"dependencies": {
    "babel-core": "^6.7.4",
    "express": "^4.13.4",
    "body-parser": "^1.15.0"
  },
  "devDependencies": {
    "babel-cli": "^6.11.4",
    "babel-polyfill": "^6.13.0",
    "babel-preset-es2015": "^6.6.0",
    "babel-plugin-transform-runtime": "^6.6.0",
    "chai": "^3.3.0",
    "express": "^4.13.4",
    "isparta": "^4.0.0",
    "istanbul": "^1.0.0-alpha",
    "mocha": "^2.3.3",
    "ramda": "^0.20.1",
    "sinon": "^1.17.5",
    "supertest": "^2.0.0"
  }
複製程式碼

為了要支援es6的語法,所以需要babel來進行程式碼的轉化 babel官網 ,express是一個web框架,mocha則是大部分人的首選測試框架,chai是斷言庫(讓程式碼更人性化),istanbul是一個和macha配合的非常好的輸出測試覆蓋率的庫,sinon是stub或是mock程式碼的當下最為流行框架,supertest是方面測http請求的框架,body-parser是讓你在http請求時更加方便的獲取的get或是post帶的引數。會玩的同學都懂吧,在專案路徑中npm install下,這些庫就到你的專案中了。

開發之前讓我再囉嗦一下,想讓babel正常工作,僅僅加入庫還是不行的,看我截圖,建立如下檔名的檔案,以及檔案內容(就是個配置檔案而已)有興趣可以看看babel網站的具體說明

Paste_Image.png

Node.js的入口檔案

Paste_Image.png

接下來是配置babel的最後一步了,重要可以使用es6的語法了,oh yeah

index.js 中註冊babel,意思是app.js檔案中的es6的語法都需要轉化

require('babel-core/register')
require('./app')
require('babel-polyfill')
複製程式碼

到現在你就可以寫es6的語法了

##http請求的的測試方式

我們以一個最簡單的http請求來說明, 這是一個最簡單的web server ,其中/hi 的get請求會返回welcome to hollywood!

import express from 'express'

const app = express()

app.get('/hi', (req, res) => {
    res.send('welcome to hollywood!')
})

const server = app.listen(8000,() => {
    const port = server.address().port
    console.log(`hollywood server start on http://127.0.0.1:${port}`)
})

export default server
複製程式碼

那我們就需要測試這個介面是不是真的返回的這串字串。但是在寫測試程式碼之前,還是需要配置下環境

Paste_Image.png

mocha-babel.js 的目的是測試程式碼也能使用es6語法,從截圖我們也可以看出一般來說測試程式碼的結構和實際程式碼應該大致保持一致。

test/app.js

import supertest from 'supertest'
import app from '../src/app'
import chai from 'chai'

chai.should()
var request = supertest(app)

describe('GET /hi', () => {
    it('should return text of welcome to hollywood!', (done) => {
        request.get('/hi')
        .expect((res) => {
            const result = res.text
            result.should.equals('welcome to hollywood!')
        })
        .end(done)
    })
})
複製程式碼

單元測試程式碼都是使用describe(desc: String, () => {})包裹的,第一個引數是對測試程式碼的描述,閉包是你具體要乾的事情。var request = supertest(app) 將我們要測試的server物件 包裝成supertest,可以進行get 或是post請求,並且通過expect((res) => {})中的res來進行判斷是否是我們需要的資料。在進行判斷是使用的should.equals(),則是chai庫所提供的斷言。 如果你執行npm test命令,看到如下結果,那麼恭喜你,你已經變寫成功了第一個測試程式碼, HOHO

Paste_Image.png

##測試非同步方法 一般來說我們會吧非同步的方法使用promise包裝一下,結合es7中async/await 來使得程式碼看起來像同步一樣, 可以假設一下,這個fs的writeFile方法是我們引用的第三方庫所提供的回撥方法。

src/common.js

import fs from 'fs'

export default class Common {
    
    static async writeFile(studentName) {
        const promise = new Promise((reslove, reject) => {
            const callback = (err) => {
                if (err) {
                    reject(err)
                    return
                }
                reslove()
            }
            fs.writeFile('db.txt', studentName, callback)
        })
        return promise
    }
}
複製程式碼

需要知道的是,一般第三方庫的程式碼它自己已經測試了,我們在測試使用到這個第三方哭的單元測試時,都會選擇把這個庫給mock掉,放在以後第三方庫程式碼升級導致我們測試不能通過的尷尬。

test/common.js

import Common from '../src/common'
import sinon from 'sinon'
import chai from 'chai'
import fs from 'fs'

chai.should()

let sandbox

const fsStub = () => {
    sandbox.stub(fs, 'writeFile', (filePath, text, callback) => {
        console.log('this is fake fs.writeFile()')
        callback()
    })
}

describe('Common', () => {
    beforeEach(() => {
        sandbox = sinon.sandbox.create()
        fsStub()
    })
    afterEach(() => {
        sandbox.restore()
    })

    describe('#writeFile(str)', () => {
        it('should write text to some file', async () => {
            // Given
            const text = 'some text'

            // When
            await Common.writeFile(text)
        })
    })
})
複製程式碼

sinon 這個庫就是mock物件方法而存在的,極大的提高了我們開發測試程式碼的效率

Paste_Image.png

最後,需要匯出專業的報告出我們測試程式碼的覆蓋率,需要在package.json配置一下。

Paste_Image.png

Paste_Image.png

Paste_Image.png

請自動忽略掉上圖的覆蓋率問題,僅僅是為了告訴如何配置而已。 起始測試還有很多的問題,需要你自己親手動手敲才會遇到相應的問題,但是聰明的你肯定會迎刃而解。反正我是一個感性的人,看到自己寫的程式碼全是綠色的鉤鉤是非常興奮的,也許你嘗試並堅持下來這個開發方式,你肯能也會尋到你程式碼世界的桃源!!!

相關文章