前端测试框架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 jest@24.8.0 -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 run

D:zdjjest学习文件夹Jestlesson2math.test.js:1
({"Object.":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/core@7.4.5
npm install @babel/preset-env@7.4.5 -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来管理我们的代码。

接下来安装git,安装完git之后

在命令行窗口运行下面命令

git init  //初始化git
git add.  //将当前代码存到仓库里
git commit -m 'version1' //将当前代码提交到本地仓库里面
git status //可以看到代码和提交代码改动的地方
git checkout math.test.js //将修改撤回

在jest的配置中,我们配置的是

"scripts": {
"test": "jest --watchAll",
"coverage": "jest --coverage"
},

我们可以将watchAll修改成watch,这样是和o模式的作用是相同的,jest每次都会运行和修改有关的测试用例。

 "scripts": {
    "test": "jest --watch",
    "coverage": "jest --coverage"
  },

t模式是通过正则表达式来过滤对应的测试用例。

进入t模式,会有一个pattern >输入toMatch字符串就会执行与toMatch有关的测试用例(名称)

2.5 异步代码的测试方法

在项目目录下新建文件fetchData.js,测试异步代码需要使用axios,执行下面命令

npm install axios@0.19.0 --save

文件fetchData.js就可以使用axios调用异步接口了,文件内容如下:

import axios from "axios";

export const fetchData = (fn) => {
  //   {
  //     "success": true
  //   }
  axios.get("http://www.dell-lee.com/react/api/demo.json").then((response) => {
    fn(response.data);
  });
};

新建测试文件fetchData.test.js

import { fetchData } from "./fetchData";

test("fetchData 返回结果是{success:true}", () => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
  });
});

执行npm run test命令之后,发现测试用例通过,

将fetchData.js文件里面的demo.json变成demo1.json时,发现测试用例也能通过,但其实改成这样之后,接口是不通的,会返回404。

为什么呢?是因为测试用例执行时,发现fetchData函数能够成功地执行,测试用例就结束了,就不会走到fetchData里面的函数里面,就没有执行expect函数了,执行时没有等到回调函数执行结束才会认为测试用例执行结束。

当测试回调式的异步函数时,上面的写法是有问题的,可以改成下面的代码

import { fetchData } from "./fetchData";

test("fetchData 返回结果是{success:true}", (done) => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
    done();
  });
});
//给异步回调函数添加一个参数done,只有当done执行结束之后,异步回调函数才算执行结束。

接下来介绍另外一种异步函数

import axios from "axios";

// 回调类型异步函数
// export const fetchData = (fn) => {
//   //   {
//   //     "success": true
//   //   }
//   axios.get("http://www.dell-lee.com/react/api/demo.json").then((response) => {
//     fn(response.data);
//   });
// };

//异步函数的第2种形式
export const fetchData = (fn) => {
  //   {
  //     "success": true
  //   }
  return axios.get("http://www.dell-lee.com/react/api/demo2.json");
};

测试代码如下:

import { fetchData } from "./fetchData";

// 回调类型异步函数的测试
test("fetchData 返回结果是{success:true}", (done) => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true,
    });
    done();
  });
});

// 第2种异步函数的测试(返回promise对象的异步函数)
test("fetchData 返回结果是{success:true}", () => {
  //记得当fetchData函数返回的是promise对象的时候
  //和then方法做正确性测试的时候,一定要记得在前面使用return返回一下

  return fetchData().then((response) => {
    expect(response.data).toEqual({
      success: true,
    });
  });
});

// 第2种异步函数的测试(返回promise对象的异步函数)
test("fetchData 返回结果为404", () => {
  //记得当fetchData函数返回的是404的时候
  //expect.assertions(1)表示下面的expect语句至少执行一遍
  //如果不加expect.assertions(1)的话,当promise对象返回不是404的时候
  //下面语句执行也会返回true,因为根本没有执行catch语句
  expect.assertions(1);
  return fetchData().catch((e) => {
    console.log(e.toString());
    expect(e.toString().indexOf("404") > -1).toBe(true);
  });
});

// 第2种异步函数的另外一种测试方法(返回promise对象的异步函数)
test("fetchData 返回结果是{success:true}", () => {
  //toMatchObject匹配器表示只要返回的对象包含toMatchObject里面的内容
  //就返回true,toMatchObject函数的参数是返回对象的子集

  return expect(fetchData()).resolves.toMatchObject({
    data: {
      success: true,
    },
  });
});

test("fetchData 返回结果为404", () => {
  //当请求可以调通的时候,测试用例不会通过,因为这是测试请求不通的情况,如下
  //Received promise resolved instead of rejected,

  //当请求不通的时候,测试用例可以通过
  //比上面的写法更简单
  return expect(fetchData()).rejects.toThrow();
});

// 第2种异步函数的第3种测试方法(返回promise对象的异步函数)
test("fetchData 返回结果是{success:true}", async () => {
  //用await代替return
  //要注意await要和async一起使用
  await expect(fetchData()).resolves.toMatchObject({
    data: {
      success: true,
    },
  });
});

test("fetchData 返回结果为404", async () => {
  //当请求可以调通的时候,测试用例不会通过,因为这是测试请求不通的情况,如下
  //Received promise resolved instead of rejected,

  //当请求不通的时候,测试用例可以通过
  //比上面的写法更简单
  await expect(fetchData()).rejects.toThrow();
});

// 第2种异步函数的第4种测试方法(返回promise对象的异步函数)
test("fetchData 返回结果是{success:true}", async () => {
  //用await代替return
  //要注意await要和async一起使用
  const response = await fetchData();
  expect(response.data).toEqual({
    success: true,
  });
});

test("fetchData 返回结果为404", async () => {
  //当请求不通的时候,测试用例可以通过
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    console.log(e.toString());
    expect(e.toString()).toEqual("Error: Request failed with status code 404");
  }
});

关于异步函数的测试方法基本上就有这么多了。

2.6 Jest中的钩子函数

在目录下新建文件Counter.js和Counter.test.js,

Counter.js文件内容如下:

class Counter {
  constructor() {
    this.number = 0;
  }
  addOne() {
    this.number += 1;
  }
  minusOne() {
    this.number -= 1;
  }
}

export default Counter;

Counter.test.js文件内容如下:

import Counter from "./Counter";
//Jest中如果想要在测试用例执行之前,做一些准备

//创建Counter类的对象
const counter = new Counter();
test("测试Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("测试Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(0);
});
//这2个测试用例中,counter使用的是同一个实例对象,因此这2个测试用例彼此受到影响。

如何解决上面的问题呢?使用钩子函数来解决,是jest里面要对测试内容基础化处理。Counter.test.js文件内容如下:

import Counter from "./Counter";
//Jest中如果想要在测试用例执行之前,做一些准备

//创建Counter类的对象
let counter = null;
beforeAll(() => {
  console.log("BeforeAll");
  //beforeAll是在所有测试用例执行之前被jest调用的函数
  counter = new Counter();
});

beforeEach(() => {
  //在每个测试用例执行之前,该函数都会被jest调用执行
  //这样就可以解决多个测试用例互相影响的问题
  counter = new Counter();
});

afterEach(() => {
  //在每个测试用例执行之后,该函数都会被jest调用执行
  counter = new Counter();
});

afterAll(() => {
  //等待所有的测试用例执行结束之后会被jest调用的函数
});
test("测试Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("测试Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

接下来给Counter.js再添加2个方法,如下代码

class Counter {
  constructor() {
    this.number = 0;
  }
  addOne() {
    this.number += 1;
  }
  minusOne() {
    this.number -= 1;
  }

  addTwo() {
    this.number += 2;
  }

  minusTwo() {
    this.number -= 2;
  }
}

export default Counter;

Counter.test.js文件内容也要加上相应的方法测试内容,代码如下:

import Counter from "./Counter";
//Jest中如果想要在测试用例执行之前,做一些准备

//创建Counter类的对象
let counter = null;
beforeAll(() => {
  console.log("BeforeAll");
  //beforeAll是在所有测试用例执行之前被jest调用的函数
  counter = new Counter();
});

beforeEach(() => {
  //在每个测试用例执行之前,该函数都会被jest调用执行
  //这样就可以解决多个测试用例互相影响的问题
  counter = new Counter();
});

afterEach(() => {
  //在每个测试用例执行之后,该函数都会被jest调用执行
  counter = new Counter();
});

afterAll(() => {
  //等待所有的测试用例执行结束之后会被jest调用的函数
});
test("测试Counter 中的addOne方法", () => {
  counter.addOne();
  expect(counter.number).toBe(1);
});

test("测试Counter 中的minusOne方法", () => {
  counter.minusOne();
  expect(counter.number).toBe(-1);
});

test("测试Counter 中的addTwo方法", () => {
  counter.addTwo();
  expect(counter.number).toBe(2);
});

test("测试Counter 中的minusTwo方法", () => {
  counter.minusTwo();
  expect(counter.number).toBe(-2);
});

但是如果我们想让加法相关的方法放到一起,减法相关的方法放到一起,这时,就会用到分组的概念,引入describe相关的概念,如下代码,这样测试代码层次更加清晰

import Counter from "./Counter";

describe("测试Counter相关的代码", () => {
  //Jest中如果想要在测试用例执行之前,做一些准备
  //创建Counter类的对象
  let counter = null;
  beforeAll(() => {
    console.log("BeforeAll");
    //beforeAll是在所有测试用例执行之前被jest调用的函数
    counter = new Counter();
  });

  beforeEach(() => {
    //在每个测试用例执行之前,该函数都会被jest调用执行
    //这样就可以解决多个测试用例互相影响的问题
    counter = new Counter();
  });

  afterEach(() => {
    //在每个测试用例执行之后,该函数都会被jest调用执行
    counter = new Counter();
  });

  afterAll(() => {
    //等待所有的测试用例执行结束之后会被jest调用的函数
  });

  describe("测试增加相关的代码", () => {
    test("测试Counter 中的addOne方法", () => {
      counter.addOne();
      expect(counter.number).toBe(1);
    });

    test("测试Counter 中的addTwo方法", () => {
      counter.addTwo();
      expect(counter.number).toBe(2);
    });
  });

  describe("测试减少相关的代码", () => {
    test("测试Counter 中的minusOne方法", () => {
      counter.minusOne();
      expect(counter.number).toBe(-1);
    });

    test("测试Counter 中的minusTwo方法", () => {
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    });
  });
});

执行上面的测试代码,运行结果如下,这样看起来就很清晰了。

PASS ./Counter.test.js
测试Counter相关的代码
测试增加相关的代码
√ 测试Counter 中的addOne方法 (3ms)
√ 测试Counter 中的addTwo方法 (1ms)
测试减少相关的代码
√ 测试Counter 中的minusOne方法 (1ms)
√ 测试Counter 中的minusTwo方法 (1ms)

console.log Counter.test.js:8
BeforeAll

上面提到的BeforeAll等这些钩子函数,其实可以写在每一个describe函数里面,也就是说每个describe函数里面都可以定义这些钩子函数,对其下面的每个测试用例(test函数)都生效的。

每次运行的时候,先执行外部的钩子函数,再执行内部的钩子函数。

在有很多测试用例执行的时候,我们很难发现用例中的问题,可以用test.only函数只执行这个测试用例,而不是执行其他的测试用例。

import Counter from "./Counter";

describe("测试Counter相关的代码", () => {
  //Jest中如果想要在测试用例执行之前,做一些准备
  //创建Counter类的对象
  let counter = null;
  beforeAll(() => {
    console.log("BeforeAll");
    //beforeAll是在所有测试用例执行之前被jest调用的函数
    counter = new Counter();
  });

  beforeEach(() => {
    //在每个测试用例执行之前,该函数都会被jest调用执行
    //这样就可以解决多个测试用例互相影响的问题
    counter = new Counter();
  });

  afterEach(() => {
    //在每个测试用例执行之后,该函数都会被jest调用执行
    counter = new Counter();
  });

  afterAll(() => {
    //等待所有的测试用例执行结束之后会被jest调用的函数
  });

  describe("测试增加相关的代码", () => {
    test.only("测试Counter 中的addOne方法", () => {
      counter.addOne();
      expect(counter.number).toBe(1);
    });

    test("测试Counter 中的addTwo方法", () => {
      counter.addTwo();
      expect(counter.number).toBe(2);
    });
  });

  describe("测试减少相关的代码", () => {
    test("测试Counter 中的minusOne方法", () => {
      counter.minusOne();
      expect(counter.number).toBe(-1);
    });

    test("测试Counter 中的minusTwo方法", () => {
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    });
  });
});

执行结果如下:

PASS ./Counter.test.js
测试Counter相关的代码
测试增加相关的代码
√ 测试Counter 中的addOne方法 (4ms)
○ skipped 测试Counter 中的addTwo方法
测试减少相关的代码
○ skipped 测试Counter 中的minusOne方法
○ skipped 测试Counter 中的minusTwo方法

准备型的代码一定要钩子函数中,而不要直接放在describe函数中,因为describe函数中的代码会先被执行,然后再执行钩子函数中的方法。

2.7 Jest中的Mock

在文件目录下新增demo.js文件和demo.test.js文件

demo.js文件内容如下:

export const runCallback = (callback) => {
  callback("abc");
};

demo.test.js文件内容如下:

import { runCallback } from "./demo";

test("测试 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要测试成功,就要在之前的函数代码里面添加return
  //   //这样就修改了之前的函数代码
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函数来解决这个问题
  const func = jest.fn();
  //测试callback函数,如果在执行之后,func函数被调用
  //说明callback函数被成功执行了
  runCallback(func);
  expect(func).toBeCalled();
  //测试用例通过,说明func函数被调用
  //mock函数,可以捕获函数的调用
  //   console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //如果测试func被调用2次,可以通过calls属性,[]里面是函数的参数,当修改callback函数的参数为'abc'时
  runCallback(func);
  //   console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: undefined },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
  //判断函数调用的次数
  expect(func.mock.calls.length).toBe(2);
  //判断函数执行的参数
  expect(func.mock.calls[0]).toEqual(["abc"]);
});

当我们想让Mock的函数有返回值时候,可以采用下面的形式

import { runCallback } from "./demo";

test("测试 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要测试成功,就要在之前的函数代码里面添加return
  //   //这样就修改了之前的函数代码
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函数来解决这个问题
  const func = jest.fn(() => {
    return "456";
  });
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: '456' } ]
  //   }
  
});

也可以采用下面的形式

import { runCallback } from "./demo";

test("测试 runCallback", () => {
  //使用jest提供的mock函数来解决这个问题
  //也可以使用下面的形式
  const func = jest.fn();
  //第1次调用返回值
  func.mockReturnValueOnce("Dell");
  //   //第2次调用返回值
  //   func.mockReturnValueOnce("Dell").mockReturnValueOnce("Dell");
  //   //每次调用返回值
  //   func.mockReturnValue("Dell");
  runCallback(func);
  console.log(func.mock);
  //测试用例通过
  expect(func.mock.results[0].value).toBe("Dell");
  
  //   {
  //     calls: [ [ 'abc' ] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: 'Dell' } ]
  //   }
  runCallback(func);
  console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: 'Dell' },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
});

从mock函数打印的内容可以看出,invocationCallOrder属性表示函数调用顺序;还有一个属性instances,表示mock函数中this的指向,接下来通过一个例子来看下这个属性

demo.js文件中添加一个创建对象的函数,代码如下:

export const runCallback = (callback) => {
  callback("abc");
};

export const createObject = (classItem) => {
  new classItem();
};

修改demo.test.js文件,来测试新增加的函数,文件内容如下

import { runCallback, createObject } from "./demo";

test.only("测试createObject函数", () => {
  const func = jest.fn();
  createObject(func);
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ mockConstructor {} ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //从打印结果看出instances属性里面有东西了,是mockConstructor,
  //因为传入的是mock函数对象,this就指向mock的函数
  //因为mock函数是被当成构造函数去执行的,所以this指向mock的构造函数
  //而上面函数的测试用例中,mock函数执行时this指向undefined
});

上面的例子可以看出,

mock函数

  1. 可以捕获函数的调用和返回结果,以及this指向和执行顺序;
  2. 可以让我们自由设置函数的返回结果
  3. 还可以改变内部函数的实现

接下来通过异步函数分析一下第3点:

demo.js文件内容如下:

import axios from "axios";

export const runCallback = (callback) => {
  callback("abc");
};

export const createObject = (classItem) => {
  new classItem();
};

export const getData = () => {
  return axios.get("/api").then((res) => res.data);
};

demo.test.js文件内容如下:

import { runCallback, createObject, getData } from "./demo";
import Axios from "axios";
jest.mock("axios");
//写了上面的代码,jest就不会去请求真正的数据了

test("测试 runCallback", () => {
  //   const func = () => {
  //     return "hello";
  //   };
  //   //如果想要测试成功,就要在之前的函数代码里面添加return
  //   //这样就修改了之前的函数代码
  //   expect(runCallback(func)).toBe("hello");

  //使用jest提供的mock函数来解决这个问题
  //也可以使用下面的形式
  const func = jest.fn();
  //第1次调用返回值
  func.mockReturnValueOnce("Dell");
  //   //第2次调用返回值
  //   func.mockReturnValueOnce("Dell").mockReturnValueOnce("Dell");
  //   //每次调用返回值
  //   func.mockReturnValue("Dell");
  runCallback(func);
  console.log(func.mock);
  expect(func.mock.results[0].value).toBe("Dell");
  //也可以用下面的形式来确认函数调用的参数,和上面的是等价的
  //expect(func).toBeCalledWith("abc");
    
  //   {
  //     calls: [ [ 'abc' ] ],
  //     instances: [ undefined ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: 'Dell' } ]
  //   }
  runCallback(func);
  console.log(func.mock);
  //   {
  //     calls: [ [ 'abc' ], [ 'abc' ] ],
  //     instances: [ undefined, undefined ],
  //     invocationCallOrder: [ 1, 2 ],
  //     results: [
  //       { type: 'return', value: 'Dell' },
  //       { type: 'return', value: undefined }
  //     ]
  //   }
});

test("测试createObject函数", () => {
  const func = jest.fn();
  createObject(func);
  console.log(func.mock);
  //   {
  //     calls: [ [] ],
  //     instances: [ mockConstructor {} ],
  //     invocationCallOrder: [ 1 ],
  //     results: [ { type: 'return', value: undefined } ]
  //   }
  //从打印结果看出instances属性里面有东西了,是mockConstructor,
  //因为传入的是mock函数对象,this就指向mock的函数
  //因为mock函数是被当成构造函数去执行的
});

test.only("测试getData函数", async () => {
  //一般在真实的项目中,测试异步函数时不会真正发送ajax请求
  //模拟axios调用请求返回值
  //mock,第3个用处是改变函数的内部实现
  Axios.get.mockResolvedValue({ data: "hello" });
  //只模拟一次
  // Axios.get.mockResolvedValueOnce({ data: "hello" });
  await getData().then((data) => {
    expect(data).toBe("hello");
  });
});

mock函数还可以使用下面的形式来定义,mockImplementation函数来定义函数。

test("测试 runCallback", () => {
  //使用jest提供的mock函数来解决这个问题
  //也可以使用下面的形式
  const func = jest.fn();
  //   每次调用返回值
  //   func.mockReturnValue("Dell");
  //也可以用下面的方式来实现
  func.mockImplementation(() => {
    return "Dell";
  });

  //   func.mockImplementationOnce(() => {
  //     return "Dell";
  //   });

  runCallback(func);
  console.log(func.mock);
  //断言
  expect(func.mock.results[0].value).toBe("Dell");
});

当mock函数返回this的时候,可以用下面的形式

import { runCallback, createObject, getData } from "./demo";
import Axios from "axios";
jest.mock("axios");
//写了上面的代码,jest就不会去请求真正的数据了

test("测试 runCallback", () => {
  //使用jest提供的mock函数来解决这个问题
  //也可以使用下面的形式
  const func = jest.fn();
  //希望函数做了一些事情,但最后返回this
  func.mockImplementation(() => {
    return this;
  });
  //mock函数返回的this是undefined的
  //上面的mock函数返回this相当于mockReturnThis函数
  //   func.mockReturnThis();
  runCallback(func);
  console.log(func.mock);
  //断言
  expect(func.mock.results[0].value).toBeUndefined();
});

vs中安装jest插件,该插件自动运行test文件,不用每次在命令行npm run start命令了,而且执行测试用例有问题,会在控制台中出现错误信息,非常方便。

3.Jest难点深入

3.1 snapshot功能测试

snapshot一般用于对配置文件做测试,在项目目录下新建demo.js文件和demo.test.js文件

demo.js文件内容如下:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080
    }
}

对demo.js文件的测试文件demo.test.js代码如下:

import {
    generateConfig
} from './demo';


test('测试generateConfig函数', () => {
    //传统写法
    expect(generateConfig()).toEqual({
        server: 'http://localhost',
        port: 8080
    })
})

如果我们想增加配置项,往demo.js再添加配置项的时候,我们也要去修改对应的测试文件

如下代码:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost'
    }
}

测试文件也要修改

import {
    generateConfig
} from './demo';


test('测试generateConfig函数', () => {
    //传统写法
    expect(generateConfig()).toEqual({
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost'
    })
})

这样太麻烦了,其实Jest提供了另外一种方式,toMatchSnapshot,如下的代码所示:

toMatchSnapshot测试当前函数的执行结果和快照相等

当第一次运行的时候, 并没有任何快照

第一次保存运行的时候, 会帮我们生成一个快照, 在目录下多了一个_snapshots的文件夹

当再次执行代码的时候, 生成一个新的snapshot,然后和老的snapshot做比较, 如果一样的话, 测试用例就会通过,否则不会通过

toMatchSnapshot匹配器一般用于测试配置文件, 配置文件一般不会变

当修改了配置文件之后,测试用例就会报错提示,这时提醒我们是否确认本次修改,确认之后再更新快照内容(或者执行之后按下u就可以更新快照)

import {
    generateConfig
} from './demo';


test('测试generateConfig函数', () => {
    //传统写法
    // expect(generateConfig()).toEqual({
    //     server: 'http://localhost',
    //     port: 8080,
    //     domain: 'localhost'
    // })

    expect(generateConfig()).toMatchSnapshot();
})

如果环境上有多个快照呢,这时候我们增加一个配置文件,demo.js文件内容如下:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: '201'
    }
}

export const generateAnotherConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: '2019'
    }
}

demo.test.js文件内容如下:

import {
    generateConfig,
    generateAnotherConfig
} from './demo';


test('测试generateConfig函数', () => {
    expect(generateConfig()).toMatchSnapshot();
})


test('测试generateAnotherConfig函数', () => {
    expect(generateAnotherConfig()).toMatchSnapshot();
})

执行npm run test命令之后,如果我们修改了多个配置文件的内容,按下u会更新所有的快照,如果我们想交互式确实每个快照的内容呢?可以按下i,一个一个逐一确认。

上面的配置文件里面time是写死的,但如果它是动态生成的时间呢?如下代码:

export const generateConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: new Date()
    }
}

export const generateAnotherConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        domain: 'localhost',
        time: new Date()
    }
}

那我们每次保存快照更新之后,每次都不相同,下一次运行的结果和快照内容还不相同,怎么解决呢?如下代码:

import {
    generateConfig,
    generateAnotherConfig
} from './demo';

test('测试generateConfig函数', () => {
    expect(generateConfig()).toMatchSnapshot({
        time: expect.any(Date)
        //Date也可以写成Number,String之类的
    });
});

//上面代码表示toMatchSnapshot里面的time字段匹配成任意的Date


test('测试generateAnotherConfig函数', () => {
    expect(generateConfig()).toMatchSnapshot({
        time: expect.any(Date)
    });
})

接下来尝试使用行内的snapshot,在项目中安装prettier,如下命令

npm install prettier@1.18.2 --save

将上面的demo.test.js文件中的toMatchSnapshot改成toMatchInlineSnapshot函数后,再次保存运行之后,会发现快照的内容没有出现在snapshot文件夹,而是出现在demo.test.js文件中了,如下所示:

import {
    generateConfig,
    generateAnotherConfig
} from "./demo";

test("测试generateConfig函数", () => {
    expect(generateConfig()).toMatchInlineSnapshot({
            time: expect.any(Date)
            //Date也可以写成Number,String之类的
        },
        `
    Object {
      "domain": "localhosts",
      "port": 8080,
      "server": "http://localhost",
      "time": Any<Date>,
    }
  `
    );
});

test("测试generateAnotherConfig函数", () => {
    expect(generateAnotherConfig()).toMatchInlineSnapshot({
            time: expect.any(Date)
        },
        `
    Object {
      "domain": "localhosts",
      "port": 8080,
      "server": "http://localhost",
      "time": Any<Date>,
    }
  `
    );
});

3.2 mock深入学习

在项目目录下新建demo.js文件和demo.test.js文件,

demo.js文件代码如下:

import axios from 'axios';

export const fetchData = () => {
    return axios.get('/').then(res => res.data);
}

//接口返回
// {
//     data: '(function(){retutrn '123 '})()'
// }

demo.test.js文件代码如下,这种测试方法是2.7章节中提到的对axios进行模拟。

import {
    fetchData
} from './demo';
import Axios from 'axios';

jest.mock('axios');
test('fetchData测试', () => {
    Axios.get.mockResolvedValue({
        data: "(function(){return '123'})()"
    })
    return fetchData().then(data => {
        //用eval是因为返回的数据结果是字符串函数,需要执行一下字符串函数,得到函数执行返回的结果
        expect(eval(data)).toEqual('123');
    })
});

接下来我们介绍其他的模拟方式

可以在本地自己写个函数来替代发送请求的函数,在同级目录下(和demo.js同级目录)创建文件夹__ mocks __,(在其下面创建demo.js文件来替代之前写的demo.js文件),该目录下的demo.js文件内容如下:

注意:这里的模拟的目录只能在同级目录,不在同级目录就不会找到对应的模拟文件了。

//这样异步代码的测试就修改成同步代码的测试了
export const fetchData = () => {
    return new Promise((resolved, reject) => {
        resolved(
            "(function(){return '123'})()"
        )
    })
}

demo.test.js文件里面就可以使用我们模拟文件下的文件里面的函数了,这样遇到fetchData函数就不会使用之前demo.js文件里面的函数了,而是使用__ mocks __文件夹下面的demo.js文件里面的fetchData函数了。

jest.mock('./demo');
//让jest模拟当前目录下demo.js的内容,jest会自动去__mocks__目录下去找模拟的demo.js内容

import {
    fetchData
} from './demo';

test('fetchData测试', () => {
    return fetchData().then(data => {
        //用eval是因为返回的数据结果是字符串函数,需要执行一下字符串函数,得到函数执行返回的结果
        expect(eval(data)).toEqual('123');
    })
});

jest不但提供了mock函数,还提供了unmock函数,

jest.mock('./demo');和 jest.unmock('./demo');

在Jest的config文件里面有个配置项,叫做automock,将其设置为automock:true之后,和jest.mock函数效果是一样的

利用Jest.mock功能mock这个文件的时候有个问题,就是这个文件里面的有些函数需要模拟,有些函数不需要模拟,如何解决这个问题呢,看下面的例子,在demo.js文件添加一个函数

import axios from 'axios';

export const fetchData = () => {
    return axios.get('/').then(res => res.data);
}

export const getNumber = () => {
    return '123';
}

在demo.test.js文件里面添加测试代码,如下所示:

jest.mock('./demo');
//让jest模拟当前目录下demo.js的内容,jest会自动去__mocks__目录下去找模拟的demo.js内容

import {
    fetchData,
    getNumber
} from './demo';

test('fetchData测试', () => {
    return fetchData().then(data => {
        //用eval是因为返回的数据结果是字符串函数,需要执行一下字符串函数,得到函数执行返回的结果
        expect(eval(data)).toEqual('123');
    })
});

test('getNumber', () => {
    expect(getNumber()).toEqual('123');
});

运行代码发现报错了,这是因为我们模拟的demo文件里面并没有getNumber()函数,怎么解决这个问题呢?

在写测试代码的时候,我们希望模拟异步函数,但是同步函数进不进行模拟了,同步函数就用之前的函数进行测试,如下代码,使用真实的getNumber函数,使用jest.requireActual函数获取真实的函数。

jest.mock('./demo');
//让jest模拟当前目录下demo.js的内容,jest会自动去__mocks__目录下去找模拟的demo.js内容

import {
    fetchData
} from './demo';
const {
    getNumber
} = jest.requireActual('./demo');

test('fetchData测试', () => {
    return fetchData().then(data => {
        //用eval是因为返回的数据结果是字符串函数,需要执行一下字符串函数,得到函数执行返回的结果
        expect(eval(data)).toEqual('123');
    })
});

test('getNumber', () => {
    expect(getNumber()).toEqual('123');
});

3.3 mock timers

3.4 ES6中对类的测试

本小节通过对类的Mock理解单元测试和集成测试。

在根目录下新建文件util.js文件和util.test.js文件,

util.js文件内容如下,它是一个工具类,里面的每个方法都比较复杂。

class Util {
    init() {
        //...复杂的逻辑
    }
    a() {
        //...复杂的逻辑

    }
    b() {
        //...复杂的逻辑

    }
}
export default Util;

对Util类中的方法进行测试,其实也比较简单,如下util.test.js文件的代码:

import Util from './util';

let util = null;
beforeAll(() => {
    util = new Util();
})
test('测试a方法', () => {
    // expect(util.a(1, 2)).toBr('12');

})

新建demo.js文件和demo.test.js文件,demo.js文件代码如下:

import Util from './util';


const demoFunction = (a, b) => {
    const util = new Util();
    util.a(a);
    util.b(b);
}

export default demoFunction;

对demo.js文件进行测试:

执行demoFunction的时候,会创建一个Util实例,然后这个实例又去调用a方法和b方法

调用a方法和b方法的时候,时间比较长,会特别耗费性能,而且a和b的执行对测试来说是没什么帮助的

我们只需要知道a方法和b方法执行过就可以了,不去执行原始的a方法和b方法,就可以节约性能

这里我们对Util类进行模拟

demo.test.js文件代码如下:

jest.mock('./util');
//jest.mock函数发现util是一个类,jest会做一件事情,会自动将类中的构造函数和方法都自动地替换成jest.fn(),如下面的代码所示
//通过jest.mock,我们就可以对函数进行追溯了 
// const Util = jest.fn();
// Util.a = jest.fn();
// Util.b = jest.fn();
// Util.init = jest.fn();

import Util from './util';
//为什么要import Util呢?因为demo.js里面运行的demoFunction函数里面的Util是从这个目录引入的Util,mock的也是这个目录下面的Util

import demoFunction from './demo';


test('测试demoFunction', () => {
    demoFunction();
    expect(Util).toHaveBeenCalled();
    console.log(Util.mock);

    //  {
    //      calls: [
    //          []
    //      ],
    //      instances: [Util {
    //          init: [Function],
    //          a: [Function],
    //          b: [Function]
    //      }],
    //      invocationCallOrder: [1],
    //      results: [{
    //          type: 'return',
    //          value: undefined
    //      }]
    //  }

    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
})

从上面例子可以看出,关于Util类中a方法和b方法的测试应该放在对Util类的测试,对demo.js文件中的demoFunction的测试的重点不是方法a和方法b是如何执行的,a和b方法执行耗时,而且测试也不需要a和b都执行,而是a和b被执行过,所以我们通过mock方法的形式对a和b进行模拟。

单元测试是指对一个独立的模块单元进行的测试,而这个模块依赖其他模块的,我们并不关心,可以使用mock来提升测试性能。

集成测试不仅是对本单元,还对包含的单元统一进行的测试。

mock可以让我们引入其他模块变得更加简单。

最后再说一个有关mock的模拟方法,上面代码中的jest.mock函数主要是通过jest.fn函数进行的函数模拟

jest.mock('./util');

//jest.mock函数发现util是一个类,jest会做一件事情,会自动将类中的构造函数和方法都自动地替换成jest.fn(),如下面的代码所示

//通过jest.mock,我们就可以对函数进行追溯了

// const Util = jest.fn();

// Util.a = jest.fn();

// Util.b = jest.fn();

// Util.init = jest.fn();

我们还可以自己实现Util类和Util类中的函数,在和util.js文件同级目录下新建__ mock __目录,然后在目录下新建util.js文件,代码内容如下;

const Util = jest.fn();
Util.prototype.a = jest.fn();
Util.prototype.b = jest.fn();
Util.prototype.init = jest.fn();

export default Util;

这样代码中的mock部分就由我们自己实现了,我们还可以自己去mock函数的实现,来实现mock函数的自定义,如下面的代码所示,自定义Util类以及方法的实现。

const Util = jest.fn(() => {
    console.log('constuctor');
});
Util.prototype.a = jest.fn(() => {
    console.log('a');
});

Util.prototype.b = jest.fn();
Util.prototype.init = jest.fn();

export default Util;

还有一种写法可以让我们自定义mock函数的实现,这样运行测试用例的时候,就是我们自己mock的util中的内容了,util.test.js文件代码 如下所示:

jest.mock('./util', () => {
    const Util = jest.fn(() => {
        console.log('constuctor--');
    });
    Util.prototype.a = jest.fn(() => {
        console.log('a');
    });

    Util.prototype.b = jest.fn();
    Util.prototype.init = jest.fn();
    return Util;
});

import Util from './util';

import demoFunction from './demo';


test('测试demoFunction', () => {
    demoFunction();
    expect(Util).toHaveBeenCalled();

    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
})

3.5 Jest中对DOM节点操作的测试

因为要对DOM节点做测试,需要安装jquery,执行下面命令

npm install jquery --save

在根目录下面新建demo.js和demo.test.js文件,demo.js代码如下

import $ from 'jquery';

const addDivToBody = () => {
    $('body').append('<div/>');
}

export default addDivToBody;

demo.test.js文件代码如下:

import addDivToBody from './demo';
import $ from 'jquery';


test('测试 addDivToBody', () => {
    addDivToBody();
    //打印输出dom元素的长度
    // console.log($('body').find('div').length);
    //1
    expect($('body').find('div').length).toBe(1);
    addDivToBody();
    // console.log($('body').find('div').length);
    //2
    expect($('body').find('div').length).toBe(2);
})

上面代码可以看出,jest可以通过jquery对dom元素做一些测试,为什么呢?

是因为jest运行的环境是node环境,

node环境不具备dom,

jest在node环境下自己模拟了一套dom的api,称作jsDom

4.React中的TDD和单元测试

4.1 什么是TDD

TDD的开发流程:(Red-Green development)

1.编写测试用例;

2.运行测试,测试用例无法通过测试;

3.编写代码,使测试用例通过测试

4.优化代码,完成开发

5.重复上述步骤

TDD优势

1.长期减少回归bug;

2.代码质量良好(组织,可维护性)

3.测试覆盖率高,一般测试覆盖率在80%,90%,不能做到100%

4.错误测试代码不容易出现

接下来通过一个TodoList项目来了解TDD的流程

原文地址:https://www.cnblogs.com/zdjBlog/p/13121168.html