Jest前端自动化测试框架难点进阶
一、snapshot快照测试
新建 lesson9.js
export const generateConfig = () => {
return {
server: 'http://localhost',
port: 8080
}
}
新建 lesson9.test.js
import { generateConfig } from "./lesson9";
test('测试 generateConfig', async () => {
expect(generateConfig()).toEqual({
server: 'http://localhost',
port: 8080
})
})
对于这种配置文件最简单的测试思路就是如上面这样写,但是这样的话每次配置文件有改动都需要相应修改测试案例,并不友好,因此快照功能应运而生!!!
修改 lesson9.test.js
test('测试 generateConfig', async () => {
expect(generateConfig()).toMatchSnapshot();
})
运行,日志如下:
PASS ./lesson9.test.js
√ 测试 generateConfig (6ms)
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 written, 1 total
Time: 3.697s
toMatchSnapshot: 匹配快照,首次执行会在测试案例文件所在目录下生成一个__snapshots__
目录,目录下有个文件lesson9.test.js.snap:。
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`测试 generateConfig 1`] = `
Object {
"port": 8080,
"server": "http://localhost",
}
`;
若是下次测试生成快照文件与之前不同,则测试不会通过,试一下:
修改 lesson9.js:
export const generateConfig = () => {
return {
server: 'http://localhost',
port: 8080,
alisa: {}
}
}
运行测试用例,日志如下:
FAIL src/lesson9/__tests__/lesson9.test.js
× 测试 generateConfig (10ms)
● 测试 generateConfig
expect(received).toMatchSnapshot()
Snapshot name: `测试 generateConfig 1`
- Snapshot
+ Received
Object {
+ "alisa": Object {},
"port": 8080,
"server": "http://localhost",
}
2 |
3 | test('测试 generateConfig', async () => {
> 4 | expect(generateConfig()).toMatchSnapshot();
| ^
5 | })
at Object.<anonymous> (src/lesson9/__tests__/lesson9.test.js:4:28)
› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with `-u` to update them.
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 1 failed, 1 total
Time: 4.32s
失败,快照不匹配,若是确定要修改,且在命令行模式,可以输入u更新快照,若是一个文件中只能有一个包含多个快照 需要输入 i 进行交互式的一个个对比更新。最后回车返回即可。
不过这样的配置文件都是写死的,若是配置文件内容是动态变化的比如:
修改 lesson9.js
export const generateConfig = () => {
return {
server: 'http://localhost',
port: 8080,
time: new Date()
}
}
这样的话每次生成的快照一定是不一样的,也就通不过测试了。
修改 lesson9.test.js:
test('测试 generateConfig', async () => {
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date)
});
})
这样的话 time 只会对比类型,不会对比值,测试就可以通过啦。
下面来试一下行内快照:
安装 prettier:
npm i prettier@1.18.2 -S
修改 lesson9.test.js(toMatchSnapshot 改为 toMatchInlineSnapshot):
test("测试 generateConfig", async () => {
expect(generateConfig()).toMatchInlineSnapshot({
time: expect.any(Date)
});
});
运行测试用例,日志如下:
PASS src/lesson9/__tests__/lesson9.test.js
√ 测试 generateConfig (27ms)
› 1 snapshot written.
› 1 snapshot obsolete.
• 测试 generateConfig 1
Snapshot Summary
› 1 snapshot written from 1 test suite.
› 1 snapshot obsolete from 1 test suite. To remove it, re-run jest with `-u`.
↳ src/lesson9/__tests__/lesson9.test.js
• 测试 generateConfig 1
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 obsolete, 1 written, 1 total
Time: 4.151s
运行过后 lesson9.test.js 变了样子:
test("测试 generateConfig", async () => {
expect(generateConfig()).toMatchInlineSnapshot(
{
time: expect.any(Date)
},
`
Object {
"port": 8080,
"server": "http://localhost",
"time": Any<Date>,
}
`
);
});
可以看到快照作为第二个参数被保存到了测试案例里。
二、mock深入学习
新建 lesson10.js
import axios from "axios";
const fetchData = () => {
return axios.get("/").then((res) => res.data);
};
export { fetchData };
新建 lesson10.test.js
import { fetchData } from "./lesson10.js";
import Axios from "axios";
jest.mock("axios");
test("fetchData 测试", () => {
Axios.get.mockResolvedValue({
data: "(function(){return '123'})()",
});
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
在jest基础入门章节中我讲过关于mock的基本知识。如上就是该知识的的体现。
如果我们不想用上面的方式去模拟接口效果,我们可以单独新建一个文件来进行模拟。
新建__mocks__
文件夹,在内部新建lesson10.js
import { fetchData } from "./lesson10.js";
import Axios from "axios";
jest.mock("axios");
test("fetchData 测试", () => {
Axios.get.mockResolvedValue({
data: "(function(){return '123'})()",
});
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
修改 lesson10.test.js:
jest.mock("./lesson10");
//这时候会去__mocks__文件夹的lesson10.js中去找fetchData
import { fetchData } from "./lesson10.js";
test("fetchData 测试", () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
还有一个方法是jest.unmock(“./lesson10”);表示取消对某个文件的模拟
下面来看看jest.config.js配置文件下的automock属性:
打开该属性改为true:
修改 lesson10.test.js(注释掉模拟代码):
//jest.mock("./lesson10");
//这时候会去__mocks__文件夹的lesson10.js中去找fetchData
import { fetchData } from "./lesson10.js";
test("fetchData 测试", () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
重启测试。输入:npm run coverage。这时候发现测试用例也会通过。这是因为automock打开了,会自动去__mock__
文件夹中找是否有fetchData,有的话就用__mock__
文件夹中的fetchData来进行模拟测试。
再看一个例子:
在lesson10.js中添加一个函数:
import axios from "axios";
const fetchData = () => {
return axios.get("/").then((res) => res.data);
};
const getNumber = () => {
return 123;
};
export { fetchData, getNumber };
修改 lesson10.test.js:
jest.mock("./lesson10");
import { fetchData, getNumber } from "./lesson10.js";
test("fetchData 测试", () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
test("getNumber 测试", () => {
expect(getNumber()).toEqual(123);
});
这时候重新运行:
之所以会失败是因为测试getNumber函数是从__mock__
文件夹中的lesson10.js中拿getNumber的。
需要改成以下方式:
jest.mock("./lesson10");
import { fetchData } from "./lesson10.js";
//取真实的lesson10文件
const { getNumber } = jest.requireActual("./lesson10");
test("fetchData 测试", () => {
return fetchData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
test("getNumber 测试", () => {
expect(getNumber()).toEqual(123);
});
三、mock timers
先看一个例子。
timer.js
export default (callback) => {
setTimeout(() => {
callback();
}, 3000);
};
timer.test.js
import timer from "./timer.js";
test("timer 测试", () => {
timer(() => {
expect(2).toBe(1);
});
});
运行之后会发现该测试用例居然通过了。因为timer是异步函数,不会等timer里面的内容执行完之后再去判断测试用例是否成功。
之前说了,可以用done来解决该问题:
import timer from "./timer.js";
test("timer 测试", (done) => {
timer(() => {
expect(2).toBe(1);
done();
});
});
这时候测试用例不通过。可以把expext中的2改为1就通过了。
但如果定时器后面的时间是几百秒,那这样子就太复杂了。可以用下面的方式:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1);
});
修改timer.js:
export default (callback) => {
setTimeout(() => {
callback();
setTimeout(() => {
callback();
}, 3000);
}, 3000);
};
再去执行测试:
测试用例不通过是因为这时候执行了两次。
改为2就可以通过了:
但有时候我们想只让外层的定时器执行,可以这样子:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
jest.runOnlyPendingTimers();
expect(fn).toHaveBeenCalledTimes(1);
});
我们可以改进一下:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(1);
});
这时候测试通过。
修改代码:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(2000); //时间快进2s
expect(fn).toHaveBeenCalledTimes(1);
});
这时候不通过。因为只快进了2s,还差1s。
修改代码:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(6000); //时间快进6s
expect(fn).toHaveBeenCalledTimes(1);
});
这时候不通过。因为快进了6s,这时候是执行了两次的。
修改代码:
import timer from "./timer.js";
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(2);
});
这时候是通过的。
由上面可知,我们会发现如果有多个test进行toHaveBeenCalledTimes,可能会影响其它的测试用例。这时候我们就需要在beforeEach钩子中重置一下timer。
import timer from "./timer.js";
beforeEach(() => {
//在该文件中遇到setTimeout这种就用假的Timer模拟
jest.useFakeTimers();
});
test("timer 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(2);
});
test("timer1 测试", () => {
const fn = jest.fn();
timer(fn);
// jest.runAllTimers();
// jest.runOnlyPendingTimers();
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(1);
jest.advanceTimersByTime(3000); //时间快进3s
expect(fn).toHaveBeenCalledTimes(2);
});
四、ES6中类的测试
创建一个util.js,util类中的方法逻辑很复杂:
class Util {
a() {
//逻辑极其复杂
}
b() {
//逻辑极其复杂
}
}
export default Util;
对a、b两个方法的测试代码在util.test.js:
import Util from "./util.js";
let util = null;
beforeEach(() => {
util = new Util();
});
test("测试 a方法", () => {
// expect(util.a(1,2)).toBe('12')
});
test("测试 b方法", () => {
// expect(util.b(1,2)).toBe('1212')
});
之后创建demo.js:
import Util from "./util.js";
const demoFunction = (a, b) => {
let util = new Util();
util.a(a);
util.b(b);
};
export default demoFunction;
在demo.js中有一个demoFunction方法,该方法中会创建一个Util实例,然后执行Util实例中的方法a和b。
于是我们对demoFunction测试。新建demo.test.js,在这个测试文件里面,我们的重点不是要测试Util实例中a、b方法的细节,而是测试demoFunction方法中的Util被执行,并且Util的a、b方法执行即可。a、b两个方法内部逻辑极其复杂,如果真正执行会十分耗费性能。所以不如模拟a、b方法:
jest.mock("./util");
// jest发现util是一个类,会自动把类的构造函数和方法变成jest.fn(),会做以下转换
// let Util = jest.fn();
// Util.a = jest.fn();
// Util.b = jest.fn();
import Util from "./util";
import demoFunction from "./demo";
test("测试 demoFunction方法", () => {
demoFunction();
expect(Util).toHaveBeenCalled();
console.log(Util.mock.instances[0]);
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
});
我们会发现我们经常在上述的单元测试中用到mock,它可以提升性能。
我们也可以自己实现对Util的模拟。在__mocks__
文件夹新建util.js:
let Util = jest.fn(() => {
console.log("constructor");
});
Util.prototype.a = jest.fn(() => {
console.log("a方法");
});
Util.prototype.b = jest.fn(() => {
console.log("b方法");
});
export default Util;
这样子也可以测试:
五、DOM节点操作的测试
安装jquery:
npm install jquery --save
新建domDemo.js:
import $ from "jquery";
const addDivToBody = () => {
$("body").append("<div/>");
};
export default addDivToBody;
新建domDemo.test.js:
import addDivToBody from "./domDemo.js";
import $ from "jquery";
test("测试 addDivToBody方法", () => {
addDivToBody();
addDivToBody();
expect($("body").find("div").length).toBe(2);
});
//本来在node环境是没有dom的
//但jest在node环境下自己模拟了一套dom的api