前言
mini-vue
断断续续写了快了大半年了,reactivity
部分其实我很早就完成了,但是runtime-core
这部分我之前写了1周就弃坑了。
原因是什么?大概是 reactivity
有 vitest
的单元测试帮我把关,让我重构和后续功能的实现都比较有把握。
但是为什么 runtime-core
不继续这样呢?原因是 vitest
运行在 nodejs
环境,这样子很多windows
的 api
都无法使用。
动机
如果你看过vue源码你大概知道会有 custom-renderer
这东西,然后配合 happy-dom
去把 runtime-dom
部分重新实现一遍就好了。又或者学习一下vue源码通过序列化的方式去做测试。
可能你会问:你既然都知道了怎么还不去弄?
我的回答是:我后面学完了才知道的。
我当初的决定是自己仿一个简单的 vitest
去做我 runtime-core
部分的测试。
功能列表
- it
- only
- skip
- expect
- not
- toBe
- toEqual
- toContain
- toHaveBeenCalled
- toHaveBeenCalledTimes
- beforeEach
- afterEach
- vi
- fn
暂时不支持嵌套和mock,因为没想做的太复杂,目前就这些暂时够我用了。
具体实现
最主要原理就是要收集it注册的测试用例,然后通过一个开关启动。如果expect的结果不正确就抛错,告诉用户期望的和实际的值是什么。
it & expect & runner
大致的框架如下
// test-runner.js
// 存放测试用例
const testCases = [];
/**
* 注册测试用例
* @param {*} name 用例名
* @param {*} cb 回调函数
* @param {*} options 选项
*/
export function it(
name,
cb,
options = {
only: false,
skip: false,
}
) {
testCases.push({
name,
cb,
options,
});
}
/**
* 断言
* @param {any} value
*/
export const expect = (value) => {
return {
toBe(val) {
if (value !== val) {
throw new Error(`expect "${val}" but got "${value}"`);
}
},
// ... 其他的断言函数
};
/**
* 启动器
* @param {*} name 用例名
* @param {*} cb 回调函数
* @param {*} options 选项
*/
export async function runner(unitName) {
let successNums = 0;
let failNums = 0;
let total = runCases.length;
console.log(`======== ${unitName} start ========`);
for (const cases of testCases) {
const { name, cb } = cases;
console.log(` ===== ${name} start ====`);
try {
await cb();
successNums++;
console.log(" | > 结果:测试通过");
} catch (error) {
console.error(error);
failNums++;
} finally {
}
console.log(` ===== ${name} end ====`);
}
console.log(` ===== ${name} end ====`);
console.log(`共成功用例 ${successNums} / ${total}`);
}
用法
使用 live-server
打开
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script type="module" src="test-add.js"></script>
</html>
// test-add.js
import {it, expect, runner} from "./test-runner.js";
function add(a,b) {
return a + b;
}
it("test-add success", () => {
const c = add(1,2);
expect(c).toBe(3);
});
it("test-add fail", () => {
const c = add(1,2);
expect(c).toBe(4);
});
runner("test add");
beforeEach & afterEach
大差不差,也是一个全局变量去收集
const testCases = [];
// 收集beforeEach和afterEach
const beforeEachCb = [];
const afterEachCb = [];
export async function runner(unitName) {
let successNums = 0;
let failNums = 0;
let total = runCases.length;
console.log(`======== ${unitName} start ========`);
for (const cases of testCases) {
const { name, cb } = cases;
console.log(` ===== ${name} start ====`);
// 每个case之前执行
beforeEachCb.forEach((fn) => {
fn();
});
try {
await cb();
console.log(" | > 结果:测试通过");
successNums++;
} catch (error) {
console.error(error);
failNums++;
} finally {
// 放到finally中就可以完成后置
afterEachCb.forEach((fn) => {
fn();
});
}
console.log(` ===== ${name} end ====`);
}
console.log(`======== ${unitName} end ========`);
console.log(`共成功用例 ${successNums} / ${total}`);
cleanup();
console.log("\n");
}
export const beforeEach = (cb) => {
beforeEachCb.push(cb);
};
export const afterEach = (cb) => {
afterEachCb.push(cb);
};
it.skip & it.only
这边其实只是给case加些标志方便区分。
export async function runner(unitName) {
// 把那些需要skip掉的cases排出去
let runCases = testCases.filter((c) => c.options.skip !== true);
// 过滤出那些only的cases
let onlyCases = testCases.filter((c) => c.options.only === true);
// 如果有设置only,就执行的用例就是那些设置了only的
if (onlyCases.length > 0) {
runCases = onlyCases;
}
let successNums = 0;
let failNums = 0;
let total = runCases.length;
console.log(`======== ${unitName} start ========`);
for (const cases of runCases) {
const { name, cb } = cases;
console.log(` ===== ${name} start ====`);
beforeEachCb.forEach((fn) => {
fn();
});
try {
await cb();
console.log(" | > 结果:测试通过");
successNums++;
} catch (error) {
console.error(error);
failNums++;
} finally {
afterEachCb.forEach((fn) => {
fn();
});
}
console.log(` ===== ${name} end ====`);
}
console.log(`======== ${unitName} end ========`);
console.log(`共成功用例 ${successNums} / ${total}`);
cleanup();
console.log("\n");
}
export function it(
name,
cb,
options = {
only: false,
skip: false,
}
) {
testCases.push({
name,
cb,
options,
});
}
it.only = function (name, cb) {
it(name, cb, {
only: true,
skip: false,
});
};
it.skip = function (name, cb) {
it(name, cb, {
only: false,
skip: true,
});
};
expect.not
这个也简单,只需要用到 getter
的时候做个标志即可
export const expect = (value) => {
// 是否开启not
let isSetNot = false;
return {
get not() {
isSetNot = true;
return this;
},
toBe(val) {
if (!isSetNot && value !== val) {
throw new Error(`expect "${val}" but got "${value}"`);
} else if (isSetNot && value === val) {
throw new Error(`expect not to be "${val}" but got "${value}"`);
}
},
}
vi.fn & expect.toHaveBeenCalled & toHaveBeenCalledTimes
这个vi.fn我做的比较简单,就是记录了这个fn被调用了多少次。实现起来其实做层包了个函数,并且在他被调用的时候去做记录。
// 用来存放vi.fn注册的cb
// 为什么用weakmap, 一是因为它用地址做记录不会重复,二是会自动销毁
const spyMap = new WeakMap();
export const vi = {
fn(cb) {
if (!spyMap[cb]) {
spyMap[cb] = 0;
}
return function (...args) {
// 被调用了就+1
spyMap[cb]++;
cb(...args);
};
},
};
export const expect = (value) => {
// 是否开启not
let isSetNot = false;
return {
get not() {
isSetNot = true;
return this;
},
toHaveBeenCalled() {
const callTimes = spyMap[value] || 0;
if (!isSetNot && callTimes <= 0) {
throw new Error(`expect function called but not`);
} else if (isSetNot && callTimes > 0) {
throw new Error(`expect function not to be called but it does`);
}
},
toHaveBeenCalledTimes(times) {
const callTimes = spyMap[value] || 0;
if (!isSetNot && callTimes !== times) {
throw new Error(
`expect function called "${times}" times but called "${callTimes}" times actually`
);
} else if (isSetNot && callTimes === times) {
throw new Error(
`expect function not to be called "${times}" times but it called "${callTimes}" times actually`
);
}
},
};
cleanup
最后别忘了做清除,因为你的测试用例可能分散到多个文件里,存放的cases都是在同一个全局变量中
// 清除
function cleanup() {
testCases.length = 0;
beforeEachCb.length = 0;
afterEachCb.length = 0;
}
export async function runner(unitName) {
// ...省略
console.log(`======== ${unitName} end ========`);
console.log(`共成功用例 ${successNums} / ${total}`);
cleanup();
console.log("\n");
}
结语
以上就是我自己实现的 mini-vitest
,希望对各位有帮助。另外这个东西在我的 eazy-vue3
中只是一个单独的文件存在,有需要的可以看一下我的github仓库。
github链接: