使用jest進行單元測試

豐臣正一發表於2020-08-18

以前,寫完一段程式碼我也是直接呼叫或者例項化一下,發現過了就把測試相關部分刪了。今年的不幸與坎坷使我有很長一段時間去思考人生,不想將就了,魯棒健壯的程式,開發和測試應該是分得很開的,於是我選擇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://jestjs.io/

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 國際許可協議進行許可。

相關文章