以前,寫完一段程式碼我也是直接呼叫或者例項化一下,發現過了就把測試相關部分刪了。今年的不幸與坎坷使我有很長一段時間去思考人生,不想將就了,魯棒健壯的程式,開發和測試應該是分得很開的,於是我選擇jest去做單元測試這件事。
為什麼要做單元測試
在開始之前,我們先思考這樣一個問題,我們為什麼要做單元測試?
不扯犢子直接說吧,第一點,用資料、用茫茫多的測試用例去告訴使用者,你的程式是多麼魯棒健壯;第二點,把它作為一種素養去培養吧,當你按照一系列規範去做事,那麼你做出來的東西,我想是有品質在的。
jest的安裝
在確保你的電腦裝有node環境的情況下,我們通過mkdir jest-study && cd jest-study
來初始化專案,然後我們通過npm init -y
初始化npm環境。
執行npm i jest babel-jest @babel/core @babel/preset-env
命令安裝相應的依賴包,因為後面的例子是基於ES Module的語法編寫的,所有需要安裝babel進行語法轉義。當然你也可以選擇直接用CommonJS的寫法,node天然支援的。
jest的相關配置
package.json中相關scripts
這裡筆者羅列了常用的通用的一些關於jest的指令碼,後面測試結果會陸續補充一些測試指令碼,以上的指令碼都編寫在package.json
檔案下的scripts
指令碼下面。
通用寫法
"test": "jest"
: 這個比較傻瓜式,當執行npm run test
這條命令是會去對test
目錄下的所有檔案進行相應的jest測試。
"test:help": "jest --help"
: 顧名思義,如果你不想全域性安裝jest,又想看看到底有哪些cli命令的話,就它了。
"test:debug": "jest --debug"
: 顧名思義,debug啊。
"test:verbose": "jest --verbose"
: 以層級顯示地方式在控制檯展示測試結果。
"test:noCache": "jest --no-cache"
: 顧名思義,就是設定有沒有快取,有快取的話會快點。
"test:init": "jest --init"
: 執行這句就是在根目錄建立一個jest.config.js
檔案,它在建立的時候有很多選擇項給你的。
"test:caculator": "jest ./test/caculator.test.js"
: 單檔案測試。
"test:caculator:watch": "jest ./test/caculator.test.js --watch"
: 單檔案監視測試
"test:watchAll": "jest --watchAll"
: 監視所有檔案改動,測試相應的測試。
大致基礎類的指令碼測試就總結到這裡,接下來我們看下jest.config.js
的相關配置。
jest.config.js中相關配置
裡面配置的引數太多了,有些配置了以後就可以不再package.json
檔案下寫相應的指令碼,這裡筆者閹割一部分,列舉最常見的幾個。
module.exports = {
// Automatically clear mock calls and instances between every test
clearMocks: true,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// The test environment that will be used for testing
testEnvironment: "node",
}
babel相關配置
{
"presets": [["@babel/preset-env", {
"targets": {
"node": "current"
}
}
]]
}
這裡就是配置了相應的語法轉換預設,如果後期有其他需求,可以通過plugins
去配置寫補丁轉義器,相關內容這裡就不做介紹了,可以看下筆者之前寫的關於babel的文章。
測試結果
考慮到把相關資訊打在控制檯上,第一,控制檯可能會出現一處的情況;第二,在檢視結果內容多的話可能引起眼睛不適,所有就有了樓下幾種可能。
測試覆蓋率
在package.json
中的scripts
下配置 "test:coverage": "jest --coverage"
後,然後執行相應指令碼,就會在根目錄輸出一個coverage
資料夾,裡面包含了相應的測試指令碼。當然控制檯也會輸出的。
html顯示
執行 npm i jest-html-reporter
安裝這個模組包(這裡提及一下,在npm版本大於5.x以後,可以預設不加--save這種引數),然後在jest.config.js
中配置如下:
reporters: [
"default",
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
執行相關的jest測試後,會在根目錄生成一個test-report.html
檔案,開啟後形如:
json顯示
在package.json
中配置scripts
指令碼 "test:exportJson": "jest --json --outputFile=./export/reporter.json"
,然後執行npm run test:exportJson
就會輸出相應的json報告檔案,控制檯也會以json的形式輸出相應資訊。
斷言(expect)
斷言庫的種類有很多,例如、assert、should、expect、chai等等,樓下的例子,筆者均以expect作為講解。
not
先說個最簡單的expect(received).not.toBe(expected)
,這句話的意思就是表示否對,表示我斷言、接收值不等於期望值。
toBe(expected)
這個API常用於斷言,值型別的期望值,也就是boolean、string、number、這些型別的,用它做引用型別的斷言是不合適也不可取的。
to_be.test.js
describe('#toBe', () => {
it('to be string', () => {
expect('hello world').toBe('hello world')
})
it('to be number', () => {
expect(1 + 1).toBe(2)
})
it('to be boolean', () => {
expect(true).toBe(true)
expect(false).toBe(false)
})
it('to be null', () => {
expect(null).toBe(null)
})
it('to be undefined', () => {
expect(undefined).toBe(undefined)
})
})
toEqual(expected)
通俗的理解就是等於, 可以是值型別也可以是引用型別的相等。
to_equal.test.js
test('#toEqual', () => {
expect('hello world').toEqual('hello world')
expect(110).toEqual(110)
expect(true).toEqual(true)
expect(false).toEqual(false)
expect(null).toEqual(null)
expect(undefined).toEqual(undefined)
expect([1, 2, 3, 4]).toEqual([1, 2, 3, 4])
expect({ name: 'ataola' }).toEqual({ name: 'ataola' })
})
toContain(expected) && toContainEqual(expected)
toContain()
跟的期望值是值型別的,而toContainEqual()
`可以是值型別也可以是引用型別,表示包含。
to_contain.test.js
test('#toContain', () => {
expect([1, 2, 3, 4]).toContain(1)
expect([[1, 2], [3, 4], [5, 6]]).toContainEqual([1, 2])
})
數值比較
樓下expect後面跟的英語的字面量意思就是其方法的作用,分別是,大於、大於等於、小於、小於等於、相似於(接近於),這裡值得一題的事最後一個toBeCloseTo()
,思考一下改成toBe()
可以嗎?很顯然不行,其算出來的結果是0.30000000000000004
,究其原因是js採用的是雙精度浮點表示。
number_compare.test.js
test('number compare', () => {
expect(3).toBeGreaterThan(2)
expect(3).toBeGreaterThanOrEqual(2.5)
expect(3).toBeLessThan(4)
expect(3).toBeLessThanOrEqual(3.5)
expect(0.1 + 0.2).toBeCloseTo(0.3) // <0.05 passed
})
toMatch(expected)
顧名思義,字串匹配,它支援字串和正則,/^(\w+)\1+$/
匹配的是一個字串可以由其字串通過n次組合而成的字串(leetcode一道題目),所有其匹配到的是tao
。
string_match.test.js
test('string match', () => {
expect('ataola').toMatch('ataola')
expect('ataola').not.toMatch('aloata')
expect('taotao'.match(/^(\w+)\1+$/)[0]).toMatch('tao')
})
內建的一些基本型別值
null、undefined、真假值比較特殊,所有這裡單獨有個方法表示它們。
truthiness.test.js
// toBeNull、 toBeUndefined 、 toBeDefined 、 toBeTruthy、 toBeFalsy
test('truthiness', () => {
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('i am defined').toBeDefined()
expect(true).toBeTruthy()
expect(false).toBeFalsy()
})
ToThrow(expected)
這裡是處理相關異常的, 後面可以什麼都不根,也可以跟個Error,或者相應的Error輸出資訊
exceptions.test.js
function gaoError() {
throw new Error('二營長開炮,開炮,開炮。。。')
}
test('#ToThrow', () => {
expect(gaoError).toThrow()
expect(gaoError).toThrow(Error)
expect(gaoError).toThrow('二營長開炮,開炮,開炮。。。')
})
好了,到這裡比較基礎和通用的API就介紹到這裡。接下來,我們通過自己編寫相關程式碼去鞏固下樓上的知識,這裡筆者提供兩個demo,一個是關於非同步獲取資料的斷言、一個是實現一個計算器類的斷言。
非同步
我們通過request-promise
這個庫去請求https://v1.hitokoto.cn
去獲取相應的json資料,然後進行斷言。
hitokoto.js
import rp from 'request-promise'
const getHitokoto = async () => {
const res = await rp('https://v1.hitokoto.cn')
return res
}
export default getHitokoto
hitokoto.test.js
import getHitokoto from '../src/hitokoto'
test('hitokoto', async () => {
const data = await getHitokoto()
expect(data).not.toBeNull()
})
這裡就意思下,讀者可以把data裡面的資料解構出來,進行相應的斷言。
計算器
這裡模擬了筆者手機上的計算器,實現了加減乘除清零計算等功能。
caculator.js
class CaCulator {
constructor() {
this.result = 0
}
add(...args) {
let { result } = this
result += args.reduce((pre, cur) => pre + cur)
this.result = result
return this
}
reduce(...args) {
let { result } = this
result -= args.reduce((pre, cur) => pre + cur)
this.result = result
return this
}
multiply(...args) {
let { result } = this
if (result) {
for (const val of args) {
result *= val
}
}
this.result = result
return this
}
divide(...args) {
let { result } = this
const has_zero = args.some(item => item === 0)
if (has_zero) {
result = '數學體育老師教的嗎?'
} else {
for (const val of args) {
result /= val
}
}
this.result = result
return this
}
clear() {
this.result = 0
return this
}
exec() {
const { result } = this
if (typeof result === 'string') {
this.result = 0
}
return result
}
init(n) {
this.result = typeof n === 'number' ? n : 0
return this
}
}
export default CaCulator
caculator.test.js
import Caculator from '../src/caculator'
const caculator = new Caculator()
describe('test Caculator', () => {
test('#add', () => {
expect(caculator.add(1).exec()).toBe(1)
expect(caculator.clear().add(1, 2, 3).exec()).toBe(6)
caculator.clear()
})
test('#reduce', () => {
expect(caculator.reduce(1).exec()).toBe(-1)
expect(caculator.clear().reduce(1, 2, 3).exec()).toBe(-6)
caculator.clear()
})
test('#multiply', () => {
expect(caculator.multiply(1).exec()).toBe(0)
expect(caculator.init(1).multiply(2, 3, 4, 5).exec()).toBe(120)
caculator.clear()
})
test('#divied', () => {
expect(caculator.divide(0).exec()).toBe('數學體育老師教的嗎?')
expect(caculator.divide(1, 2).exec()).toBe(0)
expect(caculator.init(100).divide(2, 2).exec()).toBe(25)
})
})
這裡筆者只是羅列了日常開發中常用的斷言API,具體的還是要參見官方文件這樣的一手資料,希望能起到拋磚引玉的效果。
參考文獻
https://github.com/Hargne/jest-html-reporter#readme
https://jestjs.io/docs/en/configuration
https://jestjs.io/docs/en/expect
https://jestjs.io/docs/en/using-matchers
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。