为什么我们要给代码编写单元测试

当有多个开发人员对代码库进行更改时,往往会出现问题和错误。也很难解决谁提交了错误代码,或者错误的根本原因是什么。因此,在将任何这些更改提交到仓库之前进行单元测试可以更好的避免这些问题。编写好的测试文件可以在开发本地测试,也可以在 CI/CD 的时候设置测试钩子。

测试分为哪些类型

单元测试

单元测试是指测试代码的最小单元,比如某个函数或者某个方法,单元测试是最容易实现的,也是平时最常见的

集成测试

集成测试是为了测试代码库中不同组件、模块之间的通信,例如不同模块之间的参数传递等。

端到端测试

端到端测试其实就是从头到尾测试软件工作流程的测试类型,当应用程序逐渐变的复杂的时候,端到端测试的复杂度也会线性上升,因此,目前很多公司都是手动测试。目前有一些工具,例如Cypress、Protractor,但是仍需要很大的工作量去配置。

作为前端开发,我们关注更多的是测试JavaScript代码,因此,本文使用 Jest 作为测试工具。
为什么是Jest,首先Jest是目前相当流行的一个JavaScript测试库 (特别是对于 react ) ,涵盖了大多数的测试场景,包括 断言,模拟和代码覆盖率测试,当使用 create-react-app 的时候其实已经内置了 Jest,今天就来带领大家入门 Jest。

开始

初始化项目

  1. mkdir jest
  2. cd jest
  3. npm init -y

安装 jest

  1. npm install jest --save-dev

让我们来编写第一个测试用例

一般函数测试

创建 script1.js

  1. const addNum = (a, b) => a + b
  2. module.exports = addNum

创建对应的测试用例 script1.test.js

  1. const addNum = require('./script1')
  2. it('Funtion test: return two numbers sum', () => {
  3. expect(addNum(3, 5)).toBe(8)
  4. expect(addNum(0, 10)).toBe(10)
  5. })

我们从 script1 中引入需要测试的方法,然后,第一个参数是测试的名称,第二个是函数,编写测试的期望。第一行的测试是 期望输入3,5的参数,得到8的结果。第二行测试是 期望输入参数0,10,结果为10。

更改package.json中的测试命令

  1. "scripts": {
  2. "test": "jest ./*test.js"
  3. }

然后运行命令

  1. npm test

会得到下面的输出

  1. Funtion test: return two numbers sum (3 ms)
  2. Test Suites: 1 passed, 1 total
  3. Tests: 1 passed, 1 total
  4. Snapshots: 0 total
  5. Time: 0.432 s, estimated 1 s
  6. Ran all test suites matching /.\/script1.test.js/i.

当你需要watch文件的时候,需要在jest命令后加上 —watch 参数即可

  1. "scripts": {
  2. "test": "jest --watch ./*test.js"
  3. }

复杂函数测试

下面我们写第二个测试 script2

  1. const findNames = (term, db) => {
  2. const matches = db.filter(names => {
  3. return names.includes(term)
  4. })
  5. // 只获取到前三个匹配项
  6. return matches.length > 3 ? matches.slice(0, 3) : matches
  7. }
  8. const functionNotTested = (term) => {
  9. return `Hello ${term}!`
  10. }
  11. module.exports = findNames

findNames函数的作用是获取db数组中匹配到的${term}关键字的前三个

functionNotTested函数只是为了测试我们测试代码的覆盖率

这个函数中有挺多的情况需要测试

  1. 提供关键词是否能返回预期
  2. 返回的预期,是否为前三个匹配到的值
    然后我们编写第二个测试文件 script2.test.js
  3. 当我们传递 null 或者 undefined 的时候,函数是否可以返回空数组
  4. 测试是否区分大小写

接着,我们构建测试脚本

  1. const findNames = require('./script2')
  2. const mockDB = [
  3. "Kamron Rhodes",
  4. "Angelina Frank",
  5. "Bailee Larsen",
  6. "Joel Merritt",
  7. "Mina Ho",
  8. "Lily Hodge",
  9. "Alisha Solomon",
  10. "Frank Ho",
  11. "Cassidy Holder",
  12. "Mina Norman",
  13. "Lily Blair",
  14. "Adalyn Strong",
  15. "Lily Norman",
  16. "Minari Hiroko",
  17. "John Li",
  18. "May Li"
  19. ]
  20. describe("Function that finds the names which match the search term in database", () => {
  21. it("Funtion test: expected search results", () => {
  22. expect(findNames("Dylan", mockDB)).toEqual([])
  23. expect(findNames("Frank", mockDB)).toEqual(["Angelina Frank", "Frank Ho"])
  24. })
  25. it("Funtion test: this should handle null or undefined as input", () => {
  26. expect(findNames(undefined, mockDB)).toEqual([])
  27. expect(findNames(null, mockDB)).toEqual([])
  28. })
  29. it("Funtion test: should not return more than 3 matches", () => {
  30. expect(findNames('Li', mockDB).length).toEqual(3)
  31. })
  32. it("Funtion test: the search is case sensitive", () => {
  33. expect(findNames('li', mockDB)).toEqual(["Angelina Frank", "Alisha Solomon"])
  34. })
  35. })

上面的 describe 是将该部分作为一个模块进行测试,然后,可以对内部函数的每个功能都做一个描述,it 只是对个别地方做测试的时候使用。然后 toEqual 并不是像 js 中的通过引用对比数组,而是每个字段相同,即为相等

接着我们写第三个例子,涉及到异步的场景
script3

  1. const fetch = require('isomorphic-fetch')
  2. const fetchPokemon = async (pokemon, fetch) => {
  3. const apiUrl = `https://pokeapi.co/api/v2/pokemon/${pokemon}`
  4. const results = await fetch(apiUrl)
  5. const data = await results.json()
  6. return {
  7. name: data.name,
  8. height: data.height,
  9. weight: data.weight
  10. }
  11. }
  12. module.exports = fetchPokemon

我们需要安装第三方依赖

  1. npm install isomorphic-fetch

这个接口是查找宠物小精灵信息的

接着,我们先不编写第三个测试文件,我们先看一下测试用例的覆盖率

  1. npm test --coverage

可以看到如下输出

  1. PASS ./script2.test.js
  2. PASS ./script1.test.js
  3. ------------|---------|----------|---------|---------|-------------------
  4. File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
  5. ------------|---------|----------|---------|---------|-------------------
  6. All files | 90 | 100 | 75 | 88.88 |
  7. script1.js | 100 | 100 | 100 | 100 |
  8. script2.js | 85.71 | 100 | 66.66 | 85.71 | 10
  9. ------------|---------|----------|---------|---------|-------------------
  10. Test Suites: 2 passed, 2 total
  11. Tests: 5 passed, 5 total
  12. Snapshots: 0 total
  13. Time: 0.5 s, estimated 1 s
  14. Ran all test suites matching /.\/script1.test.js|.\/script2.test.js/i.

注意两点

  1. 没有编写测试文件的并不会计入覆盖率统计
  2. 覆盖率统计中可以看出,未覆盖的行,以及测试覆盖率

异步函数测试

  1. const fetch = require('isomorphic-fetch')
  2. const fetchPokemon = require('./script3')
  3. it("Function test: find the Pokemon from PokeAPI and return its name, weight and height", () => {
  4. fetchPokemon("bulbasaur", fetch).then(data => {
  5. expect(data.name).toBe("bulbasaur")
  6. expect(data.height).toBe(7)
  7. expect(data.weight).toBe(69)
  8. })
  9. })

使用 npm test 进行测试,出现测试通过

  1. PASS ./script3.test.js
  2. PASS ./script2.test.js
  3. PASS ./script1.test.js
  4. Test Suites: 3 passed, 3 total
  5. Tests: 6 passed, 6 total
  6. Snapshots: 0 total
  7. Time: 0.713 s, estimated 1 s
  8. Ran all test suites matching /.\/script1.test.js|.\/script2.test.js|.\/script3.test.js/i.

真的通过了吗?我们并不知道请求成功后的断言是否执行了,所以我们需要调用 expect.assertions,来检查我们测试函数中通过了多少个断言,然后在最后一个断言结束后,调用 done 函数,如下

  1. const fetch = require('isomorphic-fetch')
  2. const fetchPokemon = require('./script3')
  3. it("Function test: find the Pokemon from PokeAPI and return its name, weight and height", () => {
  4. expect.assertions(3)
  5. fetchPokemon("bulbasaur", fetch).then(data => {
  6. expect(data.name).toBe("bulbasaur")
  7. expect(data.height).toBe(7)
  8. expect(data.weight).toBe(69)
  9. done()
  10. })
  11. })

或者我们也可以不掉用 done 方法,直接返回 fetchPokemon 这个请求。也可以达到测试效果。要牢记,在测试异步函数的时候,要测试断言的数量,以保证测试的正常

接着我们删除掉 script2 中的无用函数,再跑一遍测试,发现

  1. PASS ./script3.test.js
  2. PASS ./script2.test.js
  3. PASS ./script1.test.js
  4. ------------|---------|----------|---------|---------|-------------------
  5. File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
  6. ------------|---------|----------|---------|---------|-------------------
  7. All files | 100 | 100 | 100 | 100 |
  8. script1.js | 100 | 100 | 100 | 100 |
  9. script2.js | 100 | 100 | 100 | 100 |
  10. script3.js | 100 | 100 | 100 | 100 |
  11. ------------|---------|----------|---------|---------|-------------------
  12. Test Suites: 3 passed, 3 total
  13. Tests: 6 passed, 6 total
  14. Snapshots: 0 total
  15. Time: 2.564 s
  16. Ran all test suites matching /.\/script1.test.js|.\/script2.test.js|.\/script3.test.js/i.

至此,三个测试用例全部通过。
为代码编写测试是一个好的习惯,无论是在本地还是在 CI/CD 的过程中,有助于我们更早发现潜在的错误。

然后下篇文章,会介绍 react 组件的测试,敬请期待!