如果您觉得我们写得还不错,记得 点赞 + 关注 + 评论 三连,鼓励我们写出更好的教程💪
当我第一次听说 TDD(Test-Driven Develop, 测试驱动开发)的时候,我首先想到的是测试小姐姐们想搞事情?后面发现这个玩意儿是想干掉测试,那好,下面开始让我们干掉这群整天给我们找 bug...下面开始从简单的单元测试上手 Jest 测试框架。
本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github 仓库加星❤️哦
# 注意 powershell 中'&&'替换为';'
mkdir jest-just-go && cd jest-just-go
npm init -y
npm i jest --save-dev
npx jest --init
现在让我们正式开始,茶和图雀社区精心准备的甜品更搭哦。
在项目根目录下新建src
目录,存放我们的功能代码。然后创建src/dessert.js
。
const dessert = function (name) {
this.name = name;
}
dessert.prototype = {
enjoy: function () {
return "Enjoy the " + this.name;
}
}
module.exports = dessert;
我们已经编写了一个甜品类型,它有一个提供品尝的方法enjoy
下面开始编码,实现对上面甜品功能的单元测试。
在项目根目录下新建__tests__
目录,存放我们的测试用例。然后创建__tests__/dessert.test.js
。
const dessert = require("../src/dessert");
describe("test dessert feature", () => {
test("enjoy the cake", () => {
const cake = new dessert('cake');
expect(cake.enjoy()).toBe("Enjoy the cake");
})
})
简单的四行代码,我们的第一个测试用例就已经大功告成。这里我们只需要注意 describe
、test
、expect
这 3 个 Jest
关键字就行了:
describe
:组合同一类的 test
用例,可以添加 beforeEach \ afterEach 、beforeAll \ afterAll
(这里由于篇幅,这一类进阶特性将放在后续的教程中)为其下所有 test
进行统一描述和处理。test
:描述具体的测试用例,是单元测试的最小单元。expect
: Jest
最终落在了每一个对测试结果的 期望 上,通过 expect
中的返回值或是函数执行结果来和期望值进行对比。
回到控制台,输入:
npm test
无需更多配置,测试结果显示如下:
其中:
%Stmts
是语句覆盖率( statement coverage ):是不是每个语句都执行了?%Branch
分支覆盖率( branch coverage ):是不是每个 if 代码块都执行了?%Funcs
函数覆盖率( function coverage ):是不是每个函数都调用了?%Lines
行覆盖率( line coverage ):是不是每一行都执行了?%Stmts
和 %Lines
的区别是:行覆盖率的颗粒度是大于语句覆盖率的,因为可能允许一行中有多条语句( js 开发中尤为常见)。
我们更改__tests__/dessert.test.js
:
expect(cake.enjoy()).toBe("Enjoy the cake");
改为
expect(cake.enjoy()).toBe("enjoy the cake");
执行测试命令查看测试不通过的情形:
当我们的功能场景逐渐变得复杂,我们的测试就必须确保测试用例的覆盖率达到一个标准。最佳当然是 100%啦,这样才能保证测试小改改们找不到我们的茬,闲的没事就会主动找我们拉话话啦,美好生活从测试用例覆盖率 100%开始。
甜点不够怎么办?要不我们开家店吧!先让红豆烧和提拉米苏够吃先~
const dessert = require("./dessert");
const dessertFactory = function () {
}
dessertFactory.produce = function (type) {
switch (type) {
case 'Red bean burning':
return new dessert('Red bean burning'); // 红豆烧
case 'Tiramisu':
return new dessert('Tiramisu'); // 提拉米苏
default:
throw new Error("please choose a dessert type");
}
}
module.exports = dessertFactory;
现在想吃什么通过dessertFactory.produce(...)
order 就 fine 啦~
不过,要保证我们想吃的时候就必须能吃到(这个很重要),我们先验收先:
__tests__/dessertFactory.test.js
const dessertFactoty = require("../src/dessertFactoty");
describe("test dessertFactoty feature", () => {
test("produce all dessert", () => {
const dessertType = ['Red bean burning', 'Tiramisu'];
expect(dessertFactoty.produce(dessertType[0]).enjoy()).toBe("Enjoy the Red bean burning");
})
})
--“我不要你觉得,我要我觉得“,我要上档次的“验收报告“!
--行,网页展示出来怎么样
jest.config.js
保存测试用例覆盖率执行报告
我们在执初始化 Jest 默认配置
的时候,会生成在项目根目录下生成jest.config.js
,里面列出了所有的配置项,未设置的已经被注释掉了。我们要将每次执行测试后生成的覆盖率报告保存下来需要找到下面这项配置属性并更改:
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
然后我们可以再次执行测试命令并用浏览器打开_/coverage/lcov-report/index.html
_查看测试用例覆盖率报告:
__tests__/dessertFactory.test.js
const dessertFactoty = require("../src/dessertFactoty");
describe("test dessertFactoty feature", () => {
test("produce all dessert", () => {
const dessertType = ['Red bean burning', 'Tiramisu'];
expect(dessertFactoty.produce(dessertType[0]).enjoy()).toBe("Enjoy the Red bean burning");
expect(dessertFactoty.produce(dessertType[1]).enjoy()).toBe("Enjoy the Tiramisu");
expect(() => { dessertFactoty.produce('Luckin Coffee') }).toThrow("please choose a dessert type");
})
})
再测试并查看覆盖率报告:
这里 Functions 列
为什么不是 100%,大家可以点击 dessertFactory.js
根据详细说明分析推测。
搜索引擎上现有的 Jest
+ Typescript
的样例比较少,并且存在了一定的问题没有解决,这一部分我已经填平了坑,可以作为配置参考。
npm i ts-jest @types/jest typescript @types/node --save-dev
其中 ts-jest
为 Jest
+ Typescript
环境下进行测试提供了类型检查支持和预处理。
在根目录下创建配置文件tsconfig.json
:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"es2015"
],
"strict": true,
"declaration": true,
"outDir": "build",
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"__test__/**/*"
]
}
jest.config.js
配置
添加如下配置项:
// An array of file extensions your modules use
moduleFileExtensions: [
"js",
"json",
"jsx",
"ts",
"tsx",
"node"
],
// A preset that is used as a base for Jest's configuration
preset: "ts-jest",
src/dessert.js => src/dessert.ts
export default class dessert {
name: string;
constructor(name: string) {
this.name = name;
}
enjoy() {
return "Enjoy the " + this.name;
}
}
src/dessertFactory.js => src/dessertFactory.ts
import dessert from "./dessert";
export default class dessertFactory {
static produce(type: string) {
switch (type) {
case 'Red bean burning':
return new dessert('Red bean burning'); // 红豆烧
case 'Tiramisu':
return new dessert('Tiramisu'); // 提拉米苏
default:
throw new Error("please choose a dessert type");
}
}
}
__tests__/dessert.js => __tests__/dessert.ts
import dessert from "../src/dessert";
describe("test dessert feature", () => {
test("enjoy the cake", () => {
const cake = new dessert('cake');
expect(cake.enjoy()).toBe("Enjoy the cake");
})
})
__tests__/dessertFactory.js => __tests__/dessertFactory.ts
import dessertFactoty from "../src/dessertFactoty";
describe("test dessertFactoty feature", () => {
test("produce all dessert", () => {
const dessertType = ['Red bean burning', 'Tiramisu'];
expect(dessertFactoty.produce(dessertType[0]).enjoy()).toBe("Enjoy the Red bean burning");
expect(dessertFactoty.produce(dessertType[1]).enjoy()).toBe("Enjoy the Tiramisu");
expect(() => { dessertFactoty.produce('Luckin Coffee') }).toThrow("please choose a dessert type");
})
})
如同代码重构后我们通过测试用例可以快速检查是否改动出现差错一样,我们这次变更可以执行 Jest
测试命令,检查是否对功能无影响。
关于 Jest
测试框架中的 Mock 功能,我们主要关注两点:
mock function
: 对函数进行 mock.mock return value
: 对返回值进行 mock.
从以上两点可以衍生出 Jest
对于代码单元测试中两项常用的锋利功能:
为甜点增加了评论功能:
export default class dessert {
name: string;
static comment: string[] = [];
constructor(name: string) {
this.name = name;
}
enjoy() {
return "Enjoy the " + this.name;
}
static comments(message: string) {
dessert.comment.push(message);
}
}
专门建立一个互动区进行甜点的讨论。创建src/dessertCommentModule.ts
:
import dessert from "./dessert";
module dessertCommentModule {
export function comments(message: string) {
dessert.comments(message);
return dessert.comment;
}
}
export default dessertCommentModule;
下面开始test dessert feature with mock
~这一部分均已通过测试,建议深入研读代码,感受 Jest mock
每一处的奥妙。如果下面各处期望的结果都如你所料,那么你就是图雀社区最靓的仔:
import dessert from "../src/dessert";
import dessertCommentModule from "../src/dessertCommentModule";
jest.mock("../src/dessertCommentModule");
describe("test dessert feature", () => {
test("enjoy the cake", () => {
const cake = new dessert('cake');
expect(cake.enjoy()).toBe("Enjoy the cake");
})
})
describe("test dessert feature with mock", () => {
test("enjoy the cake with mock function", () => {
const dessertFactoryMock = jest.fn(name => new dessert(name));
const cake = dessertFactoryMock('cake');
expect(cake.enjoy()).toBe("Enjoy the cake");
expect(dessertFactoryMock.mock.results[0].value.enjoy()).toBe("Enjoy the cake");
console.log(dessertFactoryMock.mock);
})
test("enjoy the cake with mock return value", () => {
const dessertFactoryMock = jest.fn(name => new dessert(name));
const cake = new dessert('cake');
dessertFactoryMock.mockReturnValue(cake);
const tiramisu = dessertFactoryMock('tiramisu');
expect(tiramisu.enjoy()).toBe("Enjoy the cake");
expect(tiramisu).toEqual(cake);
expect(dessertFactoryMock.mock.results[0].value).toEqual(cake);
})
test("comment the dessert with mock module", () => {
const mockedDessert = dessertCommentModule as jest.Mocked<typeof dessertCommentModule>;
mockedDessert.comments.mockReturnValue(['not bad']);
expect(mockedDessert.comments("cake is so good")).toEqual(['not bad']);
expect(dessert.comment).toEqual([]);
})
test("comment the dessert with mock implementations", () => {
const mockedDessert = dessertCommentModule as jest.Mocked<typeof dessertCommentModule>;
mockedDessert.comments.mockImplementation((message: string) => {
dessert.comments(message);
return ['not bad'];
});
expect(mockedDessert.comments("cake is so good")).toEqual(['not bad']);
expect(dessert.comment).toEqual(['cake is so good']);
})
})
注意:我们可以更改test
为test.only
仅对test.only
的测试用例执行测试,降低测试时间并关注特定测试点。
test.only("enjoy the cake with mock function", () => { ...
这里我们通过 jest.fn
进行 了 mock function
功能的展示,通过执行 npm test
看到 .mock
的结构信息:
.mock
的中将会记录mock function
调用后的相关信息。
test("enjoy the cake with mock return value", () => {
....
这里我们通过 .mockReturnValu
可以将 function mock
的操作略过,直接会返回 .mockReturnValue
中填充的返回值。通过执行 npm test
验证。
import dessertCommentModule from "../src/dessertCommentModule";
jest.mock("../src/dessertCommentModule");
test.only("comment the dessert with mock module", () => {
...
通过 jest.mock
,我们 mock
了甜点评论区,这项操作可以使我们对dessertCommentModule
中的所有功能进行我们的测试定制。这里我们通过对评论功能进行 mock return value
,通过执行 npm test
看到:
并没有进入dessertCommentModule
中的comments
方法,直接返回了我们预置的返回值。
test.only("comment the dessert with mock implementations", () => {
...
我们通过对评论功能进行 mock implementations
,通过执行 npm test
看到:
进入了 mockImplementation
中的测试定制功能,并且调用了dessert
中的comments
方法。
以上。
https://jestjs.io/docs/en/getting-started
https://www.valentinog.com/blog/jest
https://dev.to/muhajirdev/unit-testing-with-typescript-and-jest-2gln
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。
本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github 仓库加星❤️哦