前端测试框架Jest总结
很多前端开源框架都有对应的测试代码,保证框架的稳定性
1.前端自动化测试产生的背景与原理
为了降低上线的bug,使用TypeScript,Flow, Eslint ,StyleLint这些工具可以实现。前端自动化测试工具普及情况不是很好。测试分为单元测试,集成测试和端到端测试。单元测试主要是对一个独立的功能单元进行的测试,通过一个小例子来了解前端自动化测试的背景。
1.1 实例引入
新建文件夹,在目录下新建index.html文件,math.js和math.test.js文件。
打开visual studio code编辑器,在index.html文件里输入英文感叹号!,然后输入tab键,将自动生成标准的html代码。
文件内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>math.js</title> <script src="math.js"></script> </head> <body> </body> </html>
小贴士:html文件运行的时候,可以使用Live Server插件,直接来运行。
在math.js中编写数据库的基本函数代码,如下代码,这时候我们将减法写错了,通过编写测试代码就可以发现这个问题。
function add(a, b) { return a + b; } function minus(a, b) { return a * b; }
测试代码写在math.test.js文件中,如下代码:
var result = add(3, 7); var expected = 10; if (result !== 10) { throw Error(`3+7应该等于${expected},但是结果却是${result}`); } var result = min(3, 3); var expected = 0; if (result !== 0) { throw Error(`3-3应该等于${expected},但是结果却是${result}`); }
怎么运行上面测试代码呢?
1.运用Live Server运行html文件,
2.在控件台中发现math.js中定义的方法都是全局的,
3.math.test.js文件中的代码,复制到控制台,发现运行结果如下,
VM256:12 Uncaught Error: 3-3应该等于0,但是结果却是9 at <anonymous>:12:11
这样自动化测试的代码就可以发现minus方法有问题,写错了。将minus方法修改成正确的方法,执行上面步骤,发现已经没有报错了。
1.2 增加代码
如果此时要在math.js文件中添加乘法的函数,代码 如下
function add(a, b) { return a + b; } function minus(a, b) { return a - b; } function multiply(a, b) { return a * b; }
再次在控制台中执行测试代码,如果发现全部执行通过,没有错误,说明新写的代码没有影响之前的代码;如果测试没通过,就说明新写的代码影响了之前的代码。
通过自动化测试可以很容易发现新增代码对之前功能的印象。
1.3 代码优化
那么我们能不能将写的这些代码简化一下,封装成公有函数呢?如下代码所示,我们希望创建一种语法来表示函数执行结果和预期值比较的结果来做测试,比上面的if,else要好很多。
expect(add(3, 3)).toBe(6); expect(minus(6, 3)).toBe(3);
接下来我们就实现这个方法,如下:
function expect(result) { return { toBe: function (actual) { if (result !== actual) { throw new Error(‘预期值和实际值不相等‘); } } } } expect(add(3, 7)).toBe(10); expect(minus(3, 3)).toBe(0);
在浏览器的控制台中执行上面代码,发现没有什么错误;
这时,如果我们将minus函数方法中的“-”变成“+”,发现控制台中有错误出现了。如下所示,但是通过这样的提示信息我们并不知道是哪个函数执行出错了,所以我们再优化一下代码。
VM570:5 Uncaught Error: 预期值和实际值不相等
at Object.toBe (:5:23)
at :12:21
优化代码如下:
function expect(result) { return { toBe: function (actual) { if (result !== actual) { throw new Error(‘预期值和实际值不相等‘); } } } } function test(desc, fn) { try { fn(); console.log(`${desc}通过测试`); } catch (e) { console.log(`${desc}没有通过测试`); } } test(‘测试加法3+7‘, () => { expect(add(3, 7)).toBe(10); }) test(‘测试减法3-3‘, () => { expect(minus(3, 3)).toBe(0); })
在控制台中执行上述代码,发现控制台输出内容如下:
测试加法3+7通过测试
测试减法3-3没有通过测试
通过上面的方式,我们就可以很容易的知道哪个方法出错了。继续优化提示信息,将预期结果和实际结果也打印出来,代码如下:
function expect(result) { return { toBe: function (actual) { if (result !== actual) { throw new Error(`预期值和实际值不相等,预期${actual}结果却是${result}`); } } } } function test(desc, fn) { try { fn(); console.log(`${desc}通过测试`); } catch (e) { console.log(`${desc}没有通过测试 ${e}`); } } test(‘测试加法3+7‘, () => { expect(add(3, 7)).toBe(10); }) test(‘测试减法3-3‘, () => { expect(minus(3, 3)).toBe(0); })
控制台中执行代码的结果如下:这样提示信息就更加完善了。
测试加法3+7通过测试
测试减法3-3没有通过测试 Error: 预期值和实际值不相等,预期0结果却是6
通过这样的方式我们就更清楚地知道了哪个函数有问题,然后去修改bug。有了这个测试函数,如果我们再想测试其他的函数,就轻松多了。
其实这个函数就是自动化测试框架Jest、Mocha、Jasmine框架的底层函数。理解了这个函数,理解自动化测试框架就会更容易理解。
2.前端自动化测试框架Jest
2.1 使用Jest修改自动化测试样例
使用Jest项目必须要有npm的包,在项目目录下运行npm init命令初始化项目的npm包。
通过下面命令安装指定版本的jest包,保存在package.json文件中的devDependencies。因为只有在开发的时候我们才会运行测试用例,上线的时候就不会运行了。
npm install -D
首先将math.test.js文件中自己定义的有关方法删除掉,因为jest里面已经定义了test方法了,就不需要我们自己定义了。删除之后的代码如下:
test(‘测试加法3+7‘, () => { expect(add(3, 7)).toBe(10); }) test(‘测试减法3-3‘, () => { expect(minus(3, 3)).toBe(0); })
之前我们定义的方法都是全局函数,如果想通过jest来做自动化测试的话,必须使用模块的形式把要测试的方法导出。使用commonJS的语法将其导出,math.test.js文件修改后的代码如下:
function add(a, b) { return a + b; } function minus(a, b) { return a + b; } function multiply(a, b) { return a * b; } module.exports = { add, minus, multiply }
在math.test.js文件中引入要测试方法,代码如下:
const math = require(‘./math.js‘); const { add, minus } = math; test(‘测试加法3+7‘, () => { expect(add(3, 7)).toBe(10); }) test(‘测试减法3-3‘, () => { expect(minus(3, 3)).toBe(0); })
接下来怎么运行math.test.js文件呢?修改package.json文件中的命令代码,如下:
"scripts": { "test": "jest" },
这样就可以通过运行npm run test命令来执行项目中的所有以test.js文件结尾的文件了。
npm run test
如下运行结果,就可以很清楚地看到执行测试用例时,哪些用例是成功的,哪些用例是失败的。
√ 测试加法3+7 (2ms) × 测试减法3-3 (2ms) ● 测试减法3-3 expect(received).toBe(expected) // Object.is equality Expected: 0 Received: 6 10 | 11 | test(‘测试减法3-3‘, () => { > 12 | expect(minus(3, 3)).toBe(0); | ^ 13 | }) at Object.<anonymous> (math.test.js:12:25) Test Suites: 1 failed, 1 total Tests: 1 failed, 1 passed, 2 total
是不是有个疑问,使用jest的时候一定要把函数导出呢?原因是Jest框架在前端自动化测中帮我们完成的是两类内容,单元测试和集成测试,单元测试是测试一个模块,是模块测试,集成测试是测试多个模块。所以使用jest的时候想测试这些内容,测试的内容一定会是模块。符合Jest的模块标准,jest才能帮助你完成测试。
在math.js文件中加入了模块的相关内容后,浏览器运行就会报错
Uncaught ReferenceError: module is not defined
at math.js:13
如何解决这个问题呢?修改代码如下,浏览器中就不会报错了。因为浏览器中会捕获异常。
try { module.exports = { add, minus, multiply } } catch (e) { }
其实现在的react和vue框架都已经集成了模块化的思想,所以实际我们并不需要通过捕获异常的方式来处理这个问题,
2.2 Jest的简单配置
2.1中我们没有对Jest进行配置,也可以执行Jest,因为Jest本身就有一些默认配置。
有时,需要对Jest的默认配置进行修改,如何进行呢?首先需要把Jest的一些配置项暴露出来,执行下面命令:
npx jest --init
该命令表示调用目录下面的nodeModule目录下面的jest命令,进行初始化。如下所示
The following questions will help Jest to create a suitable configuration for your project √ Choose the test environment that will be used for testing ? jsdom (browser-like) √ Do you want Jest to add coverage reports? ... yes √ Automatically clear mock calls and instances between every test? ... yes
进行初始化选项选择之后,目录下面 生成了jest.config.js文件,这个文件是jest的配置文件,打开文件发现我们刚才配置的一些内容被注释放开了。配置项里面有个配置项是coverageDirectory( coverageDirectory: "coverage",)项,这个是生成代码覆盖率的配置项。执行npx jest --coverage命令发现控制台会生成函数测试覆盖的结果
npx jest --coverage
PASS ./math.test.js
√ 测试加法3+7 (2ms)
√ 测试减法3-3----------|----------|----------|----------|----------|-------------------|
File % Stmts % Branch % Funcs % Lines Uncovered Line #s All files 80 100 66.67 80 math.js 80 100 66.67 80 10 ---------- ---------- ---------- ---------- ---------- ------------------- Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.986s
Ran all test suites.
于此同时发现,在项目目录下面生成了coverage文件目录,运行该目录下的index.html文件,可以看到目录下的测试代码对功能代码覆盖的百分比。
npx jest --coverage命令如果不理解的话,也可以通过下面的配置通过npm命令(npm run coverage)生成代码覆盖率的结果
"scripts": { "test": "jest", "coverage": "jest --coverage" },
将jest配置文件中的配置项( coverageDirectory: "coverage",)修改成( coverageDirectory: "delle",),删除生成的coverage文件目录,执行npm命令(npm run coverage),发现目录下面生成了delle文件夹。说明coverageDirectory配置的内容是生成的测试报告所在的文件夹名称。
在es6中不会使用commonjs的方式对模块进行导出,通常通过export和import的方式, 如何测试这种导出导入的方式呢?修改math.js文件代码如下
export function add(a, b) { return a + b; } export function minus(a, b) { return a - b; } export function multiply(a, b) { return a * b; }
修改math.test.js文件代码如下:
import { add, minus } from ‘./math‘; test(‘测试加法3+7‘, () => { expect(add(3, 7)).toBe(10); }) test(‘测试减法3-3‘, () => { expect(minus(3, 3)).toBe(0); })
进行npm run test命令之后发现报错了,如下报错
jest
FAIL ./math.test.js
● Test suite failed to runD:\zdj\jest学习文件夹\Jest\lesson2\math.test.js:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import { add, minus } from ‘./math‘; ^^^^^^ SyntaxError: Cannot use import statement outside a module at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17) at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
为什么会有上面的报错呢?是因为运行Jest的时候,当前环境是node环境,不认识ESModule的内容,node下面是 不支持这种语法的?
怎么解决这个问题呢?使用babel进行装换,将ESModule的代码转换成Commonjs的代码,如何转换呢?
安装babel和babel-preset
npm install @babel/ npm install @babel/ -D
要使用babel,必须要对babel进行一定的配置,在项目根目录下新建.babelrc文件,文件内容如下:
{ "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] }
这样就可以将ESModule的代码转换为支持node语法的代码了。执行npm run test命令就不会报错了。
之所以能运行,是因为执行npm run jest命令的时候,
jest内部集成了babel-jest插件,
该插件会检查当前环境是否安装了babel或babel-core,然后获取.babelrc配置
在运行测试之前,结合babel,先把测试代码进行一次转化
运行转化过的测试用例代码
2.3 Jest中的匹配器
在目录下新建matchers.test.js文件,文件内容如下:
test(‘测试‘, () => { expect(10).toBe(10); })
当每次修改代码的时候,都需要通过运行npm run test命令来执行测试用例。这样比较麻烦,在package.json文件中修改命令如下,每次修改test文件的代码,test文件就会被自动运行了。
"scripts": { "test": "jest --watchAll", "coverage": "jest --coverage" },
代码中的toBe就是一个匹配器(matchers),相当于===,如下代码是常用的适配器
test(‘测试10和10‘, () => { //toBe匹配器,类似于Object.is ===" expect(10).toBe(10); //如果是一个对象用toBe就不会通过 // const a = { // one: 1 // }; // expect(a).toBe({ // one: 1 // }); }) test(‘测试对象内容相等‘, () => { //toEqual匹配器,只是去匹配内容,这样测试用例就通过了 const a = { one: 1 }; expect(a).toEqual({ one: 1 }); }) test(‘测试toBeNull匹配器‘, () => { //toBeNull匹配器,测试用例通过 const a = null; expect(a).toBeNull(); //undefined和null不相等,测试用例不通过 // const b = undefined; // expect(b).toBeNull() }) test(‘测试undefined匹配器‘, () => { //undefined匹配器,测试用例通过 const b = undefined; expect(b).toBeUndefined(); //‘‘,null和undefined不相等,测试用例不通过 // const b = ‘‘; // expect(b).toBeUndefined(); }) //真假有关的匹配器 test(‘toBeDefined匹配器‘, () => { //a没有被定义过,测试用例不通过 // const a = undefined; // expect(a).toBeDefined(); //a被定义过,测试用例通过 const a = null; expect(a).toBeDefined(); }) //真假有关的匹配器 test(‘toBeTruthy‘, () => { //null,‘‘和0在js里面是false,测试用例不通过 // const a = null; // expect(a).toBeTruthy(); // const b = ‘‘; // expect(b).toBeTruthy(); // const b = 0; // expect(b).toBeTruthy(); //1在js里面是真,测试用例通过 const b = 1; expect(b).toBeTruthy(); }) //真假有关的匹配器 test(‘toBeFalsy‘, () => { // //1在js里面是真,测试用例不通过 // const b = 1; // expect(b).toBeFalsy(); // null,‘‘和0在js里面是false,测试用例通过 // const a = null; // expect(a).toBeFalsy(); // const b = ‘‘; // expect(b).toBeFalsy(); const b = 0; expect(b).toBeFalsy(); }) //toBeTruthy和toBeFalsy是取反的匹配器,还可以用not匹配器来对其他匹配器进行取反 test(‘not 匹配器‘, () => { const a = 1; expect(a).not.toBeFalsy(); });
除了true或者false的匹配器,还有数字相关的匹配器
//数字相关的匹配器 test(‘toBeGreaterThan匹配器‘, () => { // Expected: > 11 // Received: 10 //10比11小,测试用例不通过 // const count = 10;; // expect(count).toBeGreaterThan(11); }); test(‘toBeLessThan匹配器‘, () => { //10比11小,测试用例通过 const count = 10;; expect(count).toBeLessThan(11); }); test(‘toBeGreaterThanOrEqual匹配器‘, () => { //10比10相等,测试用例通过 const count = 10;; expect(count).toBeGreaterThanOrEqual(10); }); test(‘toBeCloseTo匹配器‘, () => { const firstNumber = 0.1; const secondNumber = 0.2 //测试用例不通过,不能用toEqual来比较2个浮点数 // Expected: 0.3 // Received: 0.30000000000000004 // expect(firstNumber + secondNumber).toEqual(0.3); // 测试用例通过 expect(firstNumber + secondNumber).toBeCloseTo(0.3); });
接下来介绍和字符串,数组,异常相关的匹配器
// 字符串相关的匹配器 test(‘toMatch‘, () => { const str = "http://www.dell-lee.com" //str字符串里面包含了‘dell‘字符串,测试用例通过 expect(str).toMatch(‘dell‘); //不仅可以写字符串,还可以写正则表达式,测试用例通过 expect(str).toMatch(/dell/); //str字符串里面不包含了‘delllee‘字符串,测试用例不通过 // expect(str).toMatch(/delllee/); }); //和Array,Set相关的匹配器 test(‘toContain‘, () => { const arr = [‘dell‘, ‘lee‘] //arr里面包含了‘dell‘字符串,测试用例通过 expect(arr).toContain(‘dell‘); //arr里面不包含‘dell‘字符串,测试用例不通过 // expect(arr).toContain(‘delle‘); // 将数组转换为set,来进行测试 const data = new Set(arr); //data里面包含了‘dell‘字符串,测试用例通过 expect(data).toContain(‘dell‘); }); //与异常相关的匹配器 const throwNewErrorFunc = () => { throw new Error(‘this is a new error‘); } test(‘toThrow‘, () => { //toThrow来判断函数是否抛出了异常 //函数抛出异常,测试用例通过 expect(throwNewErrorFunc).toThrow(); //测试抛出异常的内容是否一致 expect(throwNewErrorFunc).toThrow(‘this is a new error‘); //函数抛出异常,测试用例不通过 // expect(throwNewErrorFunc).not.toThrow(); });
除了上面提到的匹配器,jest还提供了其他的匹配器。
2.4 Jest命令行工具的使用
上面代码运行的时候,发现每次修改测试用例代码的时候,所有的测试用例都被执行了一遍。这样是比较麻烦的。执行npm run test命令的时候,会有一个Watch Usage用法,通过不同的配置执行不同的方法。
Watch Usage
? Press f to run only failed tests.
? Press o to only run tests related to changed files.
? Press p to filter by a filename regex pattern.
? Press t to filter by a test name regex pattern.
? Press q to quit watch mode.
? Press Enter to trigger a test run.
当我们在执行npm run test命令之后,按住f每次只会执行失败的测试用例,成功的测试用例会被略过,通过之后再修改,也不会执行用例了。再按f会退出f的模式,正常执行所有的测试用例。
当我们在执行npm run test命令之后,按住 o 每次只会执行和修改有关的测试用例,但是发现按下o之后执行出错了,是因为jest不知道哪些文件被修改了,如果想知道哪些文件被修改了,需要使用git来管理我们的代码。