Skip to content

Commit b9ecde6

Browse files
authored
Update spec.md
1 parent 256ef70 commit b9ecde6

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed

docs/es6/spec.md

+374
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
对于基础类库来说,保证其质量和稳定性是最重要的。开源工具的架构设计中,lodash、vue、react、ant-design,无一例外其中都包含了大量的前端自动化测试内容,用来保证类库的稳定性。
2+
3+
再从研发的整个过程来看测试集成,**测试环节是保证持续集成和交付的关键。**
4+
5+
### 测试集成
6+
7+
借用经典的软件开发测试模型--V-Model模型来说明,对测试集成的分类可以分为单元测试、集成测试、冒烟测试、验收测试4个环节。通过CI/CD流水线,其中的一些测试方式可以集成到其中。
8+
9+
### 单元测试
10+
11+
单元测试(Unit Test),属于对编码实现细节的测试,把模块、组件或者函数作为一个单元编写测试用例,来对功能做验证。单测的用例代码和模块的源码放在一起,随着模块的编译构建一起执行。
12+
13+
前端开发中经常见到的 jest、mocha都属于单元测试库。
14+
15+
### 集成测试
16+
17+
集成测试(Integration Test),属于泛指系统的功能性测试,确保了系统的所有单元可以按照预期的功能运行。把相关的组件、模块集成在 Web页面中进行统一测试,通常和端到端的一起进行。一般在模块编译构建结束后执行。集成测试关注的是产品的单独一块功能,端到端测试关注的是产品功能之前的使用链路和数据流向。
18+
19+
集成测试也可以用单测库进行。前端常用的端到端类库有PhantomJS,casperJs,puppeteer。
20+
21+
### 系统测试
22+
23+
系统测试(System Test),属于对业务系统兼容性、性能、回归、可伸缩性、安全性等方面(web应用中可能还包括网络服务、IO、物理机CPU占用、内存消耗等等)的测试。通常需要配合使用一些GUI工具进行测试,一般在做系统功能测试时同步进行。
24+
25+
### 验收测试
26+
27+
验收测试(Acceptance Test),指从一个从用户的角度出发执行的测试,因此称为验收测试。这种测试在将软件交付给客户(即生产环境)之前执行。
28+
29+
### 单元测试
30+
31+
上面已经介绍了单元测试的定义,关于单元测试有几个概念需要了解下。**测试用例**是组成单元测试模块的最小结构,也就是说把测试用例放到一个测试模块里,就是一个完整的单元测试。**断言**是测试用例中最核心的部分,比如nodejs中的 assert 模块,如果当前程序的某种状态符合 assert 的期望此程序才能正常执行,否则直接退出应用。
32+
33+
断言是单元测试框架中核心的部分,断言失败会导致测试不通过,或报告错误信息。
34+
35+
对于常见的断言,举一些例子如下:
36+
37+
- 同等性断言 Equality Asserts
38+
- expect(sth).toEqual(value)
39+
- expect(sth).not.toEqual(value)
40+
- 比较性断言 Comparison Asserts
41+
- expect(sth).toBeGreaterThan(number)
42+
- expect(sth).toBeLessThanOrEqual(number)
43+
- 类型性断言 Type Asserts
44+
- expect(sth).toBeInstanceOf(Class)
45+
- 条件性测试 Condition Test
46+
- expect(sth).toBeTruthy()
47+
- expect(sth).toBeFalsy()
48+
- expect(sth).toBeDefined()
49+
50+
用一个函数来说明:
51+
52+
```js
53+
// 待测试函数 multiple
54+
55+
function multiple(a, b) {
56+
let result = 0;
57+
for (let i = 0; i < b; ++i)
58+
result += a;
59+
return result;
60+
}
61+
62+
// 断言
63+
const assert = require('assert');
64+
assert.equal(multiple(1, 2), 2);
65+
```
66+
67+
但是nodejs 自带的 assert 模块只能满足一些简单场景的需要,而且提供的错误信息提示不太友好,其次输出的内容是程序的错误报告,而不是一个单元测试报告,所以在做单元测时需要专业的断言库提供测试报告,这样才能看到有哪些断言通过没通过。
68+
69+
### 断言库
70+
71+
断言库主要提供上述断言的语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。常见的断言库有 Should.js, Chai.js 等。
72+
73+
74+
75+
## 测试工具
76+
77+
首先要明确一点,所有的单元测试要在不同的环境下执行就要打不同环境对应的包,所以在搭建测试工具链时要确定自己运行在什么环境中,如果在 Node 中只需要加一层 babel 转换,如果是在真实浏览器中,则需要增加 webpack 处理步骤。
78+
79+
80+
81+
## mocha
82+
83+
mocha 是一个经典的测试框架,它提供了一个单元测试的骨架,可以将不同子功能分成多个文件进行测试,最后生成一份结构型的测试报告。karma是一个测试执行过程管理工具,可以watch文件更新。
84+
85+
Node 环境下测试 : mocha + chai + babel
86+
87+
浏览器环境测试 : karma + mocha + chai + webpack + babel + jsdom
88+
89+
但是mocha配置起来比较繁琐,还有一些额外的工具例如单元覆盖率(istanbul),函数模拟 (sinon.js)等辅助工具,选型的成本比较高。
90+
91+
92+
93+
## jasmine
94+
95+
jasmine 也是一个常用的测试框架,里面包含了 测试流程框架,断言函数,mock工具等测试中会遇到的工具。可以近似地看作 jasmine = mocha + chai + 辅助工具。
96+
97+
Node 环境下测试 : Jasmine + babel
98+
99+
浏览器环境测试 : karma + Jasmine + webpack + babel + jsdom
100+
101+
102+
103+
## jest
104+
105+
Jest 是 facebook 出的一个完整的单元测试技术方案,集 测试框架, 断言库, 启动器, 快照,沙箱,mock工具于一身,也是 React 官方使用的测试工具。Jest的优势是明显的:
106+
107+
速度快,具备监控模式,API 简单,易配置,隔离性好,Mock 丰富,多项目并行。
108+
109+
110+
111+
Node 环境下测试 : Jest + babel
112+
113+
浏览器环境测试 : Jest + babel + JSDOM(组件需要 [react-testing-library](https://github.com/kentcdodds/react-testing-library), [Enzyme](http://airbnb.io/enzyme/), [TestUtils](https://reactjs.org/docs/test-utils.html) 三选一) + webpack
114+
115+
### 安装
116+
117+
项目里安装 Jest 、babel
118+
119+
```bash
120+
npm install --save-dev jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest
121+
```
122+
123+
### 初始化配置
124+
125+
在根目录下生成 jest.config.js 的配置文件,scripts里添加的jest命令会在 jest.config.js 里找配置。
126+
127+
```bash
128+
jest --init
129+
```
130+
131+
配置 jest
132+
133+
```js
134+
// jest.config.js
135+
136+
module.exports = {
137+
verbose: true,
138+
roots: ['<rootDir>/packages'], // 文件入口
139+
moduleNameMapper: {
140+
'\\.(css|less|scss)$': 'identity-obj-proxy' // 使用 identity-obj-proxy mock CSS Modules
141+
},
142+
testRegex: '(/test/.*|\\.(test|spec))\\.(ts|tsx|js)$', // 匹配测试文件
143+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
144+
testPathIgnorePatterns: ['<rootDir>/node_modules/', '/node_modules/', '/lib/', '/demo/', '/dist/'],
145+
preset: 'ts-jest',
146+
testEnvironment: 'jsdom',
147+
transform: {
148+
'\\.[jt]sx?$': 'babel-jest', // 文件处理
149+
'^.+\\.svg$': 'jest-svg-transformer', // svg转换
150+
'^.+\\.(ts|tsx)$': 'ts-jest'
151+
},
152+
setupFiles: ['jest-canvas-mock'],
153+
transformIgnorePatterns: ['<rootDir>/node_modules/(?!lodash-es)'], // transform编译忽略哪些文件
154+
collectCoverage: true, // 开启收集Coverage(测试覆盖范围)
155+
coverageDirectory: '<rootDir>/coverage/', // 指定生成的coverage目录
156+
coveragePathIgnorePatterns: ['<rootDir>/coverage/'] //该路径下的测试,忽略测试覆盖率
157+
}
158+
```
159+
160+
babel配置:
161+
162+
```js
163+
{
164+
presets: [
165+
'@babel/preset-react',
166+
[
167+
'@babel/preset-env', {targets: {node: 'current'}}
168+
],
169+
'@babel/preset-typescript'
170+
],
171+
plugins: [
172+
[
173+
'import',
174+
{ libraryName: 'antd', libraryDirectory: 'es', style: true }
175+
],
176+
['@babel/plugin-transform-runtime'],
177+
['@babel/plugin-transform-modules-commonjs'] // jest不支持es模块,用babel处理
178+
]
179+
}
180+
```
181+
182+
### 基础语法
183+
184+
#### 匹配器 matchers
185+
186+
断言api。查看jest支持的所有断言api:https://www.jestjs.cn/docs/expect
187+
188+
```js
189+
// 测试相等
190+
test('two plus two is four', () => {
191+
// toBe 匹配器
192+
expect(2 + 2).toBe(4);
193+
});
194+
195+
// 测试对象相等 内容相等
196+
test('object assignment', () => {
197+
const data = {one: 1};
198+
data['two'] = 2;
199+
// toEqual 匹配器
200+
expect(data).toEqual({one: 1, two: 2});
201+
});
202+
```
203+
204+
#### 异步代码测试
205+
206+
测试异步代码的执行结果
207+
208+
```js
209+
test('the data is peanut butter', () => {
210+
return fetchData().then(data => {
211+
expect(data).toBe('peanut butter');
212+
});
213+
});
214+
215+
test('the data is peanut butter', async () => {
216+
await expect(fetchData()).resolves.toBe('peanut butter');
217+
});
218+
219+
220+
// 测试reject
221+
test('the fetch fails with an error', () => {
222+
// expect至少执行一次,如果不加则不会执行 catch
223+
expect.assertions(1);
224+
return fetchData().catch(e => expect(e).toMatch('error'));
225+
});
226+
```
227+
228+
#### 钩子函数
229+
230+
在测试运行前后进行一些设置,例如初始化测试代码等,保证每个测试的输入都是一致的。
231+
232+
```js
233+
export default class Counter {
234+
constructor (){
235+
this.number = 0;
236+
}
237+
addOne(){
238+
this.number += 1
239+
}
240+
minusOne() {
241+
this.number -= 1
242+
}
243+
}
244+
245+
test('加一', function() {
246+
counter.addOne();
247+
expert(counter.number).toBe(1)
248+
})
249+
250+
test('减一', function() {
251+
counter.minusOne();
252+
expert(counter.number).toBe(0)
253+
})
254+
255+
256+
/* ----------------------------------- */
257+
258+
259+
let counter = null;
260+
// 所有测试用例生成单独的 counter对象 保证独立
261+
beforeEach(() => {
262+
counter = new Counter();
263+
})
264+
265+
test('加一', function() {
266+
counter.addOne();
267+
expert(counter.number).toBe(1)
268+
})
269+
270+
test('减一', function() {
271+
counter.minusOne();
272+
expert(counter.number).toBe(-1)
273+
})
274+
```
275+
276+
#### mock
277+
278+
函数执行mock。对函数是否被调用,调用结果是否正常做验证。
279+
280+
```js
281+
const mockCallback = jest.fn(x => 42 + x);
282+
forEach([0, 1], mockCallback);
283+
284+
// 函数被调用两次
285+
expect(mockCallback.mock.calls.length).toBe(2);
286+
287+
// 函数第一次被调用时,第一个参数是0
288+
expect(mockCallback.mock.calls[0][0]).toBe(0);
289+
290+
// 函数第二次被调用时,第一个参数是0
291+
expect(mockCallback.mock.calls[1][0]).toBe(1);
292+
293+
// 函数第一次被调用时,返回值是42
294+
expect(mockCallback.mock.results[0].value).toBe(42);
295+
```
296+
297+
#### snapshot 快照测试
298+
299+
用来测试配置文件或组件每次渲染是否一致。例如测试输入 schema form 配置功能,获取到输出的 data 快照是否一致。如果不一致,说明本次代码修改导致 data 的输出发生变化。
300+
301+
```js
302+
it('校验配置项是否正确', () => {
303+
const user = {
304+
createdAt: new Date(),
305+
id: Math.floor(Math.random() * 20),
306+
name: 'LeBron James',
307+
};
308+
309+
expect(user).toMatchSnapshot({
310+
createdAt: expect.any(Date),
311+
id: expect.any(Number),
312+
});
313+
});
314+
315+
// Snapshot
316+
exports[`will check the matchers and pass 1`] = `
317+
Object {
318+
"createdAt": Any<Date>,
319+
"id": Any<Number>,
320+
"name": "LeBron James",
321+
}
322+
`;
323+
```
324+
325+
初次运行,生成 .snap 文件。第二次运行会校验快照是否一致。
326+
327+
#### dom 测试
328+
329+
jest 测试dom操作,内置类似jsdom的能力。
330+
331+
```js
332+
// displayUser.js
333+
const $ = require('jquery');
334+
const fetchCurrentUser = require('./fetchCurrentUser.js');
335+
336+
$('#button').click(() => {
337+
fetchCurrentUser(user => {
338+
const loggedText = 'Logged ' + (user.loggedIn ? 'In' : 'Out');
339+
$('#username').text(user.fullName + ' - ' + loggedText);
340+
});
341+
});
342+
343+
344+
// __tests__/displayUser-test.js
345+
jest.mock('../fetchCurrentUser');
346+
347+
test('验证用户登录', () => {
348+
// 初始化dom
349+
document.body.innerHTML =
350+
'<div>' +
351+
' <span id="username" />' +
352+
' <button id="button" />' +
353+
'</div>';
354+
355+
require('../displayUser');
356+
357+
const $ = require('jquery');
358+
const fetchCurrentUser = require('../fetchCurrentUser');
359+
360+
// mock 函数返回
361+
fetchCurrentUser.mockImplementation(cb => {
362+
cb({
363+
fullName: 'Johnny Cash',
364+
loggedIn: true,
365+
});
366+
});
367+
368+
// 执行按钮点击事件
369+
$('#button').click();
370+
371+
expect(fetchCurrentUser).toBeCalled();
372+
expect($('#username').text()).toEqual('Johnny Cash - Logged In');
373+
});
374+
```

0 commit comments

Comments
 (0)