自动化测试-基础知识

2021/7/19 自动化测试
// 前端自动化测试产生的背景及原理
1. 正常避免出现 Bug 的手段
  1. CodeReView
  2. 测试同学
  3. 灰度验证
2. 前端避免出现 Bug 的手段
  1. TypeScript
  2. Flow
  3. EsLint
  4. StyleLint
  5. 自动化测试工具
----------------------------------------------------------------------------------------------
// 前端自动化测试框架
1. Jasmine
2. MoCha + chai
3. Jest
4. 优点
  1. 性能好
  2. 功能强大
  3. 易用性高
----------------------------------------------------------------------------------------------
// 前端自动化测试的优势
1. 更好的代码组织,项目的可维护性增强
2. 更小的 Bug 出现概率,尤其是回归测试中的 Bug // 回归测试即修改之前的代码进行测试
3. 修改工程质量差的项目会更加安全
4. 项目具备潜在的文档特性
5. 扩展前端知识面
----------------------------------------------------------------------------------------------
// 前端自动化测试的重要知识点
1. TDDBDD
2. 集成测试、单元测试
3. 测试与业务的解耦
4. 代码测试覆盖率并不代表一定靠谱
5. 功能测试及 UI 测试
6. 测试越独立,隐藏的问题越多
----------------------------------------------------------------------------------------------
// 测试种类
1. 单元测试(针对某个单一的模块进行测试)
  1. 优点
    1. 测试覆盖率高
  2. 缺点
    1. 业务耦合度高
    2. 代码量大
    3. 过于独立
  3. 适用场景
    1. 开发函数库
2. 集成测试(针对某个单一的模块同时包括模块中的其他模块进行测试)
3. 端到端测试
4. 回归测试
5. 性能测试
6. 压力测试
----------------------------------------------------------------------------------------------
// Jest 优点
1. 速度快
2. API 简单
3. 易配置
4. 隔离性好
5. 监控模式
6. IDE 整合
7. Snapshot(快照)
8. 多项目并行
9. 覆盖率
10. Mock 丰富
----------------------------------------------------------------------------------------------
// Jest 配置
1. 使用 Jest 时,需要使用模块化代码测试如 ESMCJS
2. npm install jest
3. npx jest --init // 生成 jest.config.js 配置文件,如果不配置会走默认配置
4. npx jest --coverage // 生成覆盖率文件
5.package.json 的 script 脚本中添加 "test": "jest --watch",运行 npm run test 就会自动测试
6. Jest 命令行(只有开启了 --watch 才有效)
  1. w 键:进入或退出选择命令
  2. f 键:只会跑未通过的测试用例,已通过的不会再跑,默认是全跑
  3. o 键:如果是 --watch 默认就会自动进入 o 模式,只会跑当前修改的文件里面的测试用例,其他文件的不会再跑
           如果是 --watchAll 默认是全跑,使用时必须和 git 结合使用,否则会报错,因为 Jest 不知道哪些文件有修改
  4. a 键:和 o 键是相反的,o 是 --watch,a 是 --watchAll
  5. p 键:只有在 --watchAll 时才会出现,会根据我们输入的名字或正则去跑对应的测试用例,默认是全跑
  6. t 键:会根据我们输入的名字或正则去跑对应的测试用例
  7. q 键:退出当前的 --watch,相当于结束
  8. enter 键:重新执行所有的测试用例
----------------------------------------------------------------------------------------------
// Jest 匹配器
1. toBe 相当于 js 中的 ===
  test('测试内容是否相等', () => {
    expect({ name: 'cj' }).toBe({ name: 'cj'}) // 不通过
  })
2. toEqual 相当于 js 中的 ==
  test('测试内容是否相等', () => {
    expect({ name: 'cj' }).toEqual({ name: 'cj'}) // 通过
  })
3. toBeNull 判断内容是否是 null
  test('测试内容是否是 null', () => {
    expect(null).toBeNull() // 通过,不需要参数
  })
4. toBeUndefined 判断内容是否是 undefined
  test('测试内容是否是 undefined', () => {
    expect(undefined).toBeUndefined() // 通过,不需要参数
  })
5. toBeDefined 判断内容是否是已定义
  test('测试内容是否是 defined', () => {
    const name = undefined
    expect(name).toBeDefined() // 不通过,不需要参数,当前 name 是未定义
  })
6. toBeTruthy 判断内容是否是 true
  test('测试内容是否是 true', () => {
    expect(true).toBeTruthy() // 通过,不需要参数
  })
7. toBeFalsy 判断内容是否是 false
  test('测试内容是否是 false', () => {
    expect(false).toBeFalsy() // 通过,不需要参数
  })
8. not 取反
  test('测试内容是否是 true', () => {
    expect(false).not.toBeTruthy() // 通过,不需要参数
  })
9. toBeGreaterThan 判断 Number 内容是否比实际值大
  test('测试内容是否比 9 大', () => {
    expect(10)toBeGreaterThan(9) // 通过 10 > 9
  })
10. toBeLessThan 判断 Number 内容是否比实际值小
  test('测试内容是否比 9 小', () => {
    expect(10)toBeLessThan(11) // 通过 11 > 10
  })
11. toBeGreaterThanOrEqual 判断 Number 内容是否比实际值大或相等
  test('测试内容是否大于等于 10', () => {
    expect(10)toBeGreaterThanOrEqual(10) // 通过
  })
12. toBeLessThanOrEqual 判断 Number 内容是否比实际值小或相等
  test('测试内容是否小于等于 10', () => {
    expect(10)toBeLessThanOrEqual(10) // 通过
  })
13. toBeCloseTo 判断 Number 数值是否相等(特别是小数)
  test('测试数值是否相等', () => {
    const num1 = 0.1
    const num2 = 0.2
    expect(num1 + num2)toBeCloseTo(0.3) // 通过,如果使用 toEqual 则不通过,js 计算小数时会有误差
  })
14. toMatch 判断 String 内容是否包含实际的值
  test('测试内容是否包含实际的值', () => {
    const name = 'chenj'
    expect(name)toMatch('chen') // 通过,也可以传入正则
  })
15. toContain 判断 Array or Set 内容是否包含实际的值
  test('测试内容是否包含实际的值', () => {
    const arr = ['chen', 'jie']
    const data = new Set(arr)
    expect(arr or data)toContain('chen') // 通过
  })
16. toThrow 判断是否抛出异常
  test('测试内容是否抛出异常', () => {
    const throwErrorFn = () => {
      throw new Error('这是一个错误')
    }
    expect(throwErrorFn)toThrow('这是一个错误') // 通过,如果不传入字符串也可以通过,如果传入的字符串不是和 new Error 里面抛出的信息一致则不通过
  })
----------------------------------------------------------------------------------------------
// Jest 异步代码的测试方法
1. callback
  export const fetchData = fn => {
    axios.get('url').then(res => {
      fn(res.data)
    })
  }
  // 如果是接收一个回调函数则需要在测试的时候,需要手动的调用 done 函数,不然会直接执行完成,不会执行到 toEqual,因为异步
  test('fetchData 返回结果为 { success: true }', done => {
    fetchData(res => {
      expect(res).toEqual({ success: true })
      done()
    })
  })
2. promise
  export const fetchData = () => {
    return axios.get('url')
  }
  1. 方式一
    // then 情况(使用 return 和 toEqual 匹配器)
    test('fetchData 返回结果为 { success: true }', () => {
      return fetchData().then(res => {
        expect(res).toEqual({ success: true })
      })
    })
    // catch 情况(使用 return 和 toBe 匹配器)
    test('fetchData 返回结果为 404', () => {
      // 如果是要测试 catch 的情况,必须加上这段,表示下面测试用例至少执行一次 expect
      // 如果不加并且出现不是错误的情况就会测试不到!!!
      expect.assertions(1)
      return fetchData().catch(e => {
        expect(e.toString().indexOf('404') > -1).toBe(true)
      })
    })
  2. 方式二
    // then 情况(使用 return 和 toMatchObject 匹配器)
    test('fetchData 返回结果为 { success: true }', () => {
      // 判断返回的结果是否包含传入的对象
      return expect(fetchData()).resolves.toMatchObject({
        data: {
          success: true
        }
      })
    })
    // catch 情况(使用 return 和 toThrow 匹配器)
    test('fetchData 返回结果为 404', () => {
      // 判断返回的结果是否报错
      return expect(fetchData()).rejects.toThrow()
    })
  3. 方式三
    // then 情况(使用 async 和 toMatchObject 匹配器)
    test('fetchData 返回结果为 { success: true }', async () => {
      const res = await fetchData()
      // 判断返回的结果是否包含传入的对象
      expect(res.data).resolves.toMatchObject({
        data: {
          success: true
        }
      })
    })
    // catch 情况(使用 async 和 toBe 匹配器)
    test('fetchData 返回结果为 404', async () => {
      // 如果是要测试 catch 的情况,必须加上这段,表示下面测试用例至少执行一次 expect
      // 如果不加并且出现不是错误的情况就会测试不到!!!
      expect.assertions(1)
      try {
        await fetchData()
      } catch(e) {
        expect(e.toString().indexOf('404') > -1).toBe(true)
      }
    })
----------------------------------------------------------------------------------------------
// Jest 生命周期函数
1. beforeAll(() => {}) // 当全部的测试用例开始执行前调用
2. afterAll(() => {}) // 当全部的测试用例结束执行后调用
3. beforeEach(() => {}) // 当每个测试用例开始执行前调用
4. afterEach(() => {}) // 当每个测试用例结束执行后调用
----------------------------------------------------------------------------------------------
// Jest 分组及生命周期作用域
1. 当我们在写 test 文件时,其实外面会自动包了一层 describe 函数,如下
   也可以自己把相对应的测试用例归类到同一个分组下,或者拆分文件
    describe('全局分组', () => {
      beforeAll(() => {})
      afterAll(() => {})
      beforeEach(() => {})
      afterEach(() => {})
      describe('分组1', () => {
        test('fetchData 返回结果为 { success: true }', () => {
          return fetchData().then(res => {
            expect(res).toEqual({ success: true })
          })
        })
      })
      describe('分组2', () => {
        test('fetchData 返回结果为 404', () => {
          // 判断返回的结果是否报错
          return expect(fetchData()).rejects.toThrow()
        })
      })
    })
2. 每个 describe 下都可以写 Jest 生命周期函数,每个 describe 下写的生命周期函数的作用域只属于当前这个分组
   而全局的 describe 下的生命周期函数的作用域属于全部的分组
3. 如果写了 describe,则应该用所有的准备代码工作(如声明等)放在生命周期中,如果直接写在 describe 中
   会导致多个 describe 的执行顺序和所对应的生命周期的执行顺序不一致!!!
4. 当一个分组下的测试用例过多时,且只想单独调试某一个测试用例时,则可以使用 only 修饰符,如下
    describe('分组1', () => {
      // 写了 only 修饰符,下面的 404 测试用例不会被执行
      test.only('fetchData 返回结果为 { success: true }', () => {
        return fetchData().then(res => {
          expect(res).toEqual({ success: true })
        })
      })
      test('fetchData 返回结果为 404', () => {
          // 判断返回的结果是否报错
          return expect(fetchData()).rejects.toThrow()
        })
    })
----------------------------------------------------------------------------------------------
// Jest 中的 Mock
1. jest.fn() 特点
  1. mock 函数,可以捕获函数的调用
  2. 可以让我们通过 mockReturnValueOnce 或 mockReturnValue 自由的设置返回结果
  3. 可以改变函数内部的实现
    jest.mock('axios')
    test('测试接口', async () => {
      axios.get.mockResolvedValue({ data: 'hello' })
      await getData().then(data => { // getData 方法是用 axios 发送了一个 get 请求
        expect(data).toBe('hello')
      })
    })
2. jest.fn() 示例
  import { runCallback } from './demo'
  test('测试 runCallback', () => {
    const func = jest.fn(() => '这是返回值,可以在 func.mock 中查看,也可以不传递该函数')
    func.mockReturnValueOnce('模拟单次返回值') // 会在 func.mock 属性中记录该返回值,对应执行的次数,可以多次调用,也可链式调用
    func.mockReturnValue('模拟全部返回值') // 会在 func.mock 属性中记录该返回值,对应执行的次数
    // 会在 func.mock 属性中记录该返回值,对应执行的次数
    func.mockImplementationOnce(() => {
      console.log('额外的逻辑')
      return '模拟单次返回值'
    })
    // 会在 func.mock 属性中记录该返回值,对应执行的次数
    func.mockImplementation(() => {
      console.log('额外的逻辑')
      return '模拟全部返回值'
    })
    console.log(func.mock) // mock 属性下有这个函数对应的 this 指向、返回值、调用次数、调用栈等信息
    runCallback(func)
    expect(func).toBeCalled() // toBeCalled 匹配器可以判断 jest.fn 是否被调用
    expect(func.mock.calls[0]).toEqual(['params']) // 判断函数第一次调用时候的入参是否是 params
    expect(func).toBeCalledWith('params') // toBeCalledWith 匹配器可以判断每次调用的入参是否都是 params
  })
----------------------------------------------------------------------------------------------
// Jest 进阶
1. snapshot 快照
  // toMatchSnapshot 和 toMatchInlineSnapshot 匹配器,用于生成快照
  // 对于像 config 文件的测试,如果里面的配置一般不会变,则可以使用快照测试
  // 否则使用其他匹配器,如果配置改了,测试用例则需要也跟着改,会比较麻烦
  // toMatchSnapshot 会在目录下生成快照文件夹对应的就是所测试函数的内容映射
  // 第一次时会生成,如果后面有改变按 w + u 键可以更新本地快照即可测试通过
  // 如果一个文件中包含多个快照,在发生变化的时候可以按 w + i 单独的对每个快照进行更新
  import { generateConfig } from './demo'
  // toMatchSnapshot 会在目录下生成快照文件
  test('测试 generateConfig 函数', () => {
    expect(generateConfig()).toMatchSnapshot({
      time: expect.any(Date) // 表示 time 字段在快照中可以和源文件中的配置不一样(除了 Date,还可以是 String、Number 等)
    })
  })
  test('测试 generateConfig 函数', () => {
    // toMatchInlineSnapshot 会把快照生成在 测试用例文件中,需要配合 npm i prettier 插件进行使用
    expect(generateConfig()).toMatchInlineSnapshot({
      time: expect.any(Date) // 表示 time 字段在快照中可以和源文件中的配置不一样(除了 Date,还可以是 String、Number 等)
    })
  })
2. mock 模拟
  // 在之前学习的 mock 中,我们可以 mock axios 这个库来模拟返回数据
  // 而除了模拟返回数据还可以模拟整个函数,在目录下新建 __mocks__ 文件夹
  // 在里面新建和需要测试文件一样的名字,在测试用例文件中使用 jest.mock(./demo)
  // 除了手动的添加 jest.mock(./demo),还可以在 jest.config.js 中把 automock 配置改成 true
  // 这样 jest 就会自动到 __mocks__ 文件夹中去查找对应名称的测试方法
  // 如果在一个测试文件中有一部分需要模拟,一部分不需要,则需要的要在 __mocks__ 下写对应的方法
  // 而不需要的则在测试用例文件中通过 jest.requireActual 引入测试文件对应的方法就不会模拟
  jest.mock('./demo') // ./demo 是 __mocks__ 文件中的路径
  import { fetchData } from './demo' // 需要测试的文件路径
  const { getNumber } = jest.requireActual('./demo') // 不需要到 __mocks__ 文件夹中查找
  test('测试 fetchData 函数', () => {
    return fetchData().then(data => {
      expect(data).toEqual('123')
    })
  })
3. mock timer
  // 使用 jest.useFakeTimers() 配合 jest.runAllTimers()、jest.runOnlyPendingTimers()、jest.advanceTimersByTime()
  // 可以方便的测试定时器,toHaveBeenCalledTimes 匹配器用来判断定时器执行了几次
  import { timer } from './demo'
  jest.useFakeTimers()
  test('测试 timer 函数', () => {
    const fn = jest.fn()
    timer(fn) // timer 里面是一个 setTimeout 3000 输出 123
    jest.runAllTimers() // 执行全部的定时器不用等待
    jest.runOnlyPendingTimers() // 只执行最外层的定时器,不执行全部
    jest.advanceTimersByTime(3000) // 提前 3s 执行定时器,可以任意控制时间,注意作用域
    expect(fn).toHaveBeenCalledTimes(1) // toHaveBeenCalledTimes 匹配器用来判断定时器执行了几次
  })
4. ES6 中的类
  // 使用 jest.mock(./url),对类中的复杂的方法进行模拟
  jest.mock('./util') // 还可以和 2 中的一样在 __mocks__ 中新建模拟文件,进一步模拟
  import Util from './util' // util 中就是一个类,里面有几个复杂的方法
  import demoFn from './demo'
  test('测试 demoFn', () => {
    demoFn() // demoFn 中引用了 Util 这个类中的方法
    expect(Util).toHaveBeenCalled() // toHaveBeenCalled 匹配器用来判断是否执行
  })
5.DOM 节点操作测试
  1. Jest 是运行在 Node 环境下的,而 Node 环境是不具备 DOM
  2. Jest 在 Node 环境下模拟了一套 DOM API,使得我们可以在测试用例中
     使用 document、body 等 DOM API 进行测试
----------------------------------------------------------------------------------------------
// 扩展
1. 如果使用 VsCode 编辑器,可以安装 jest 插件,安装完成后并保证项目名称中间没有空格的情况下,写完测试用例
   就不需要再执行 npm run jest 命令,在对应文件的测试用例前就会有红绿灯告诉你测试是否通过
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
Last Updated: 2024/6/11 14:20:38
    飘向北方
    那吾克热-NW,尤长靖