Jest

2/24/2021 前端工程化前端测试Jest

Jest是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。他适用但不局限于使用以下技术的项目:Babel, TypeScript, Node, React, Angular, Vue

Jest官网 (opens new window)

# 特性:

  • 零配置
  • 快照
  • 提高性能
  • 优秀的 api
  • 代码覆盖率
  • 轻松模拟Mock Functions
  • 优秀的报错信息

# 安装使用

  1. 创建文件夹,初始化npm npm init -y
  2. 安装jest npm i --save-dev jest(如果taobao源安装不好就用npm源)
  3. 创建math.js
function sum (x, y) {
  return x + y
}

function subtract (x, y) {
  return x - y
}

exports.sum = sum
exports.subtract = subtract
1
2
3
4
5
6
7
8
9
10
  1. 创建math.test.js,把方才的代码拿过来,不过test和expect就是jest的API,而且test和expect不用引用,全局的变量,jest会注入相关的API,直接零配置就可以使用。
const { sum, subtract } = require('./math')

test('测试 sum', () => {
  expect(sum(1, 2)).toBe(3)
})

test('测试 subtract', () => {
  expect(subtract(2, 1)).toBe(1)
})
1
2
3
4
5
6
7
8
9
  1. 命令行npx jest运行测试通过
PS E:\professer\Jest\3-5-jest> npx jest
 PASS  ./math.test.js (5.943 s)
  √ 测试 sum (10 ms)
  √ 测试 subtract (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        27.459 s
1
2
3
4
5
6
7
8
9
  1. 如果把减法改成乘法运行,这里就会报错,提示是哪个地方出错。
PS E:\professer\Jest\3-5-jest> npx jest
 FAIL  ./math.test.js
  √ 测试 sum (3 ms)
  × 测试 subtract (6 ms)

  ● 测试 subtract

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 2

      6 |
      7 | test('测试 subtract', () => {
    > 8 |   expect(subtract(2, 1)).toBe(1)
        |                          ^
      9 | })

      at Object.<anonymous> (math.test.js:8:26)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.926 s, estimated 6 s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  1. 还可以修改package.json,修改script,直接用npm run test去进行测试,还可以添加一个监听参数--watchAll,自动执行测试命令
"scripts": {
    "test": "jest",
    "test:watch": "jest --watchAll"
},
1
2
3
4

# 配置文件

项目零配置就可以使用,还可以通过配置文件修改配置

  1. 命令行执行npx jest --init生成配置文件
# 是否用ts的语法创建配置文件
? Would you like to use Typescript for the configuration file? ... no
# 选择环境,jsdom可以兼容node
? Choose the test environment that will be used for testing » - Use arrow-keys. Return to submit.
    node
>   jsdom (browser-like)
# 你是否想添加测试报告
? Do you want Jest to add coverage reports? » (y/N) y
# 你想通过哪个provider去提供代码覆盖率
? Which provider should be used to instrument code for coverage? » - Use arrow-keys. Return to submit.
    v8
>   babel
# 是否在测试之前自动清除mock的数据
? Automatically clear mock calls and instances between every test? » (y/N) y

# 生成jest.config.js成功
📝  Configuration file created at E:\professer\Jest\3-5-jest\jest.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 生成的文档中有所有的配置项,在配置文件中也有所有的,只不过是注释着的。可以通过查询 官方配置 (opens new window) 去学习。

# Jest结合使用Babel

我们通常写项目用的是ES Module,并不是CommonJS的方式,这个时候需要通过Babel去加载功能

  1. 把math.js和math.test.js进行ES Module的转换
// math.js
export function sum (x, y) {
  return x + y
}

export function subtract (x, y) {
  return x - y
}

1
2
3
4
5
6
7
8
9
// math.test.js
import { sum, subtract } = './math'

test('测试 sum', () => {
  expect(sum(1, 2)).toBe(3)
})

test('测试 subtract', () => {
  expect(subtract(2, 1)).toBe(1)
})
1
2
3
4
5
6
7
8
9
10

这个时候直接运行会报错

  1. 这种情况就要使用babel和babel-jest (opens new window),babel-jest就是babel的适配器,jest在运行之前会调用babel把ES Module转换成CommonJS模块再来运行测试。执行 npm i babel-jest @babel/core @babel/preset-env -D
  2. 根目录创建babel.config.js,并写
// babel.config.js
module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};
1
2
3
4
  1. 这样就可以直接使用了npm run test

运行流程

npm run test 运行的jest,jest内部使用了babel-jest,其内部又使用了@babel/core, 把 es6 模块转换为 commonjs 模块,并把转换之后的模块给 jest 使用,运行测试代码

# 使用其他模块

里面都有使用了webpack,parcel,typescript的配置方式,都可以进行兼容。

更多配置 (opens new window)

# Jest中常用的匹配器

主要的API就是匹配器,toBe就是相等匹配器

# 简单匹配器

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});
1
2
3

toBe使用 Object.is 来测试精确相等,如果是检查对象类型就不好用

test('测试对象', () => {
  const obj = { foo: 'bar' }
  expect(obj).toBe({ foo: 'bar' })
})
1
2
3
4
 expect(received).toBe(expected) // Object.is equality

If it should pass with deep equality, replace "toBe" with "toStrictEqual"

Expected: {"foo": "bar"}
Received: serializes to the same string

  11 | test('测试对象', () => {
  12 |   const obj = { foo: 'bar' }
> 13 |   expect(obj).toBe({ foo: 'bar' })
     |               ^
  14 | })
1
2
3
4
5
6
7
8
9
10
11
12

这里建议使用toEqual代替,toEqual 递归检查对象或数组的每个字段:

test('测试对象', () => {
  const obj = { foo: 'bar' }
  expect(obj).toEqual({ foo: 'bar' })
})
1
2
3
4

# 有效性匹配

test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined(); // 等同于toBeDefined
  expect(n).not.toBeTruthy(); // 等同于toBeFalsy
  expect(n).toBeFalsy();
});
1
2
3
4
5
6
7
8

# 数字匹配

test('数字匹配 —— two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3); // 大于
  expect(value).toBeGreaterThanOrEqual(3.5); // 大于等于
  expect(value).toBeLessThan(5); // 小于
  expect(value).toBeLessThanOrEqual(4.5);  // 小于等于

  // toBe和toEqual也可以测试数字
  expect(value).toBe(4);  
  expect(value).toEqual(4);
});
1
2
3
4
5
6
7
8
9
10
11

浮点数的相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。

test('数字匹配 —— 两个浮点数字相加', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});
1
2
3
4
5

# 字符串匹配

toMatch 后面是正则表达式

test('字符串匹配 —— 是否不存在某个字符', () => {
  expect('team').not.toMatch(/I/); // 没有I
});

test('字符串匹配 —— 是否存在某个字符', () => {
  expect('Christoph').toMatch(/stop/);  // 有stop
});
1
2
3
4
5
6
7

# 数组和可遍历的对象匹配

通过 toContain来检查一个数组或可迭代对象是否包含某个特定项

const shoppingList = [
  'diapers',
  'kleenex',
  'trash bags',
  'paper towels',
  'milk',
];

test('数组或可迭代对象 —— 数组中是否有 milk ', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});
1
2
3
4
5
6
7
8
9
10
11
12

# 测试异常匹配

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK');
}

test('测试异常匹配', () => {
  // 函数抛出异常
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // 传入字符串,就判断消息的信息是否一致
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK'); 
  // 正则,抛出的消息里面存在JDK
  expect(() => compileAndroidCode()).toThrow(/JDK/);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 其他API

还有好多的 匹配器 (opens new window),多用用就可以熟悉。

# 异步测试

# 接口准备

  1. 安装axiosnpm i axios
  2. 去在先的接口模拟 JSONPlaceholder (opens new window),找到posts100个文章列表的接口

# 回调方式

  1. 创建async-demo.js定义接口
import axios from 'axios'

export function getPosts(callback) {
  axios.get('http://jsonplaceholder.typicode.com/posts')
    .then(res => {
      callback(res.data)
    })
}


getPosts(posts => {
  console.log(posts.length)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 创建async-demo.test.js写测试代码,记得一定要加done
import { getPosts } from './async-demo'

// 回调方式
test('post length is 100', (done) => {
  getPosts(posts => {
    expect(posts.length).toBe(100)
    // 异步执行结束了
    done()
  })
})
1
2
3
4
5
6
7
8
9
10

# Promise方式

  1. 在async-demo.js中返回promise的函数
export function getPosts2 () {
  return axios.get('http://jsonplaceholder.typicode.com/posts')
  .then(res => {
    return res.data
  })
}
1
2
3
4
5
6

2.在async-demo.test.js写测试代码,这里不加done,但是也要返回promise

import { getPosts, getPosts2 } from './async-demo'

// Promise
test('Promise posts 100', () => {
  // 务必返回 promise ,这个时候就不需要done参数了
  return getPosts2().then(posts => {
    expect(posts.length).toBe(100)
  })
})

1
2
3
4
5
6
7
8
9
10

# 使用API .resolves/.rejects

  1. 在async-demo.js中返回文章的长度,记得这里也必须使用promise
export function getPosts3 () {
  return axios.get('http://jsonplaceholder.typicode.com/posts')
  .then(res => res.data.length)
}
1
2
3
4
  1. 在async-demo.test.js写测试代码,使用这个一定也必须是promise
import { getPosts, getPosts2, getPosts3 } from './async-demo'

// .resolves/.rejects
test('.resolves posts 100', () => {
  // 务必返回promise
  return expect(getPosts3()).resolves.toBe(100)
})
1
2
3
4
5
6
7

# async/await

  1. 在async-demo.test.js写测试代码,推荐使用这种形式,更加语义化。
// async/await
test('async/await posts 100', async () => {
  const count = await getPosts3()
  expect(count).toBe(100)
})
1
2
3
4
5

# 钩子函数

# 基本用法

# 多次重复设置

  • boforeEach:每次执行前都执行这个函数
  • afterEach:每次执行后都执行这个函数
  1. 创建counter.js在里面写
export class Counter {
  constructor () {
    this.count = 0
  }
  increment () {
    this.count++
  }
  decrement () {
    this.count--
  }
  incrementTwo () {
    this.count += 2
  }
  decrementTwo () {
    this.count -= 2
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 创建counter.test.js,里面写了三个函数测试,每次都要new一个Counter类,这样比较麻烦,可以使用beforeEach钩子函数
import { Counter } from './counter'

test('Counter', () => {
  const counter = new Counter()
  expect(counter.count).toBe(0)
})

test('Counter decrement', () => {
  const counter = new Counter()
  counter.decrement()
  expect(counter.count).toBe(-1)
})

test('Counter increment', () => {
  const counter = new Counter()
  counter.increment()
  expect(counter.count).toBe(1)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 定义一个beforeEach,每个测试用例之前都会自动调用这个函数
import { Counter } from './counter'

let counter = null

beforeEach(() => {
  console.log('beforeEach')
  counter = new Counter()
})

afterEach(() => {
  console.log('afterEach')
})

test('Counter', () => {
  expect(counter.count).toBe(0)
})

test('Counter decrement', () => {
  counter.decrement()
  expect(counter.count).toBe(-1)
})

test('Counter increment', () => {
  counter.increment()
  expect(counter.count).toBe(1)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

通过测试

PASS ./async-demo.test.js (5.026 s) ● Console

console.log beforeEach

at Object.<anonymous> (counter.test.js:6:11)

console.log afterEach

at Object.<anonymous> (counter.test.js:11:11)

# 一次性设置

// 所有测试用例执行之前执行
beforeAll(() => {
  console.log('beforeAll')
})

// 所有测试用例执行之后执行
afterAll(() => {
  console.log('afterAll')
})
1
2
3
4
5
6
7
8
9

::: 执行顺序 beforeAll > beforeEach > afterEach > afterAll :::

# 作用域

  1. 在counter.test.js中对测试进行分组
describe('Counter group1', () => {
  // 加减1
  test('Counter decrement', () => {
    counter.decrement()
    expect(counter.count).toBe(-1)
  })
  
  test('Counter increment', () => {
    counter.increment()
    expect(counter.count).toBe(1)
  })
})

describe('Counter group2', () => {
  // 加减2
  test('Counter decrementTwo', () => {
    counter.decrementTwo()
    expect(counter.count).toBe(-2)
  })
  
  test('Counter incrementTwo', () => {
    counter.incrementTwo()
    expect(counter.count).toBe(2)
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  1. 如果在组内,也可以添加 beforeEach,afterEach,beforeAll,afterAll ,如果是组内的,组外的还是依然会执行,当前组的函数只会当前测试执行,group1的不会执行到group2中
describe('Counter group1', () => {

  // 当前组里面每个测试用例之前都调用
  beforeEach(() => {
    console.log('group1 before')
  })

  afterEach(() => {
    console.log('group1 after')
  })

  beforeAll(() => {
    console.log('group1 beforeAll')
  })

  afterAll(() => {
    console.log('group1 afterAll')
  })

  test('Counter decrement', () => {
    counter.decrement()
    expect(counter.count).toBe(-1)
  })
  
  test('Counter increment', () => {
    counter.increment()
    expect(counter.count).toBe(1)
  })
})

describe('Counter group2', () => {
  beforeEach(() => {
    console.log('group2 before')
  })

  afterEach(() => {
    console.log('group2 after')
  })
  
  beforeAll(() => {
    console.log('group1 beforeAll')
  })

  afterAll(() => {
    console.log('group1 afterAll')
  })

  test('Counter decrementTwo', () => {
    counter.decrementTwo()
    expect(counter.count).toBe(-2)
  })
  
  test('Counter incrementTwo', () => {
    counter.incrementTwo()
    expect(counter.count).toBe(2)
  })
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

执行顺序

全局的beforeAll > 组内的 beforeAll > 全局的 beforeEach > 组内的 beforeEach > 组内的afterEach > 全局的afterEach > 组内的afterAll > 全局的 afterAll

更新时间: 2021-09-15 12:03