持续更新中……
WHY
如何给下面的函数做单测?
'use strict'
{
getLeafCount(node) {
....
}
}
如果是
exports.getLeafCount = () => {
...
}
我们很容易利用现有单测框架做测试。但因非 CJS 也不是 ESM。我们没法拿到 getLeafCount
。
为什么加
use strict
详见:Safari iOS ReferenceError: Can't find variable: xxx。
在非模块化的开发中,即采用传统的裸写的 script 方式开发的应用如何进行单元测试? 本文提供一种思路,通过将非模块化的 JS 转成模块化的然后在 Node.js 环境中进行单测。
转换方式可以使用正则表达式也可使用 AST,本文采用 AST,相比可读性更好也更精准无误。
前言
涉及到的技术名词
- AST
技术方案
Code (非模块化)
=> AST => Code (CJS)
技术选型
- code => ast:
@babel/parser
- 遍历 ast:
@babel/traverser
- ast => code
@babel/generator
- 单测框架:Node.js 内置
import { ... } from "node:test"
一些开发技巧
.mjs
而非.js
:这样 js 文件也可以直接写import export
当然最主要是想用 top level await。// @ts-check
即使 JS 也充分利用 TS 的严格类型校验。- tsdoc:给 JS 标注类型。
如何选择生成 ast 的库?或者为什么选择 @babel/parser
?后续会讲到。
涉及到的工具
核心代码
// compile.mjs
// @ts-check
// @ts-expect-error
import { glob, readFile, mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { parse } from '@babel/parser';
import traverser from '@babel/traverse';
import generator from '@babel/generator';
// @ts-expect-error
const traverse = traverser.default;
// @ts-expect-error
const generate = generator.default;
const TAG = '@test';
console.time('[INFO] test build costs');
await main();
console.log();
console.timeEnd('[INFO] test build costs');
async function main() {
const promises = [];
// !@warn node>=22.0.0 required for `glob`
for await (const entry of glob('./static/js/**/*.js')) {
debug('Find JS file in static/js', entry);
promises.push(compile(entry));
}
return Promise.all(promises);
}
async function compile(fp) {
const content = await readFile(fp, 'utf8');
const ast = parse(content);
const fns = getFunctionsToTest(ast);
if (!fns.length) {
return;
}
info(fns.length, `functions with \`${TAG}\` comment found in`, `"${fp}".`);
const publicFunctions = fns.map(({ code, name }) => {
return makeFunctionPublic({
code,
name,
});
});
return saveToFile(publicFunctions.join('\n'), fp);
}
/**
* @param {{ code: string; name: string }} params
* @returns {string}
*/
function makeFunctionPublic({ code, name }) {
const prefix = `\nexports.${name} = ${name};`;
return prefix + '\n' + code;
}
/**
*
* @param {string} content
* @param {string} fp
*/
async function saveToFile(content, fp) {
const dist = `./tests/__auto-generated/${fp}`;
await mkdir(dirname(dist), { recursive: true });
await writeFile(
dist,
'// !Auto generated by tests/compile.mjs.\n// !Should not be modified manually.\n' +
content
);
info('Public functions saved to', `"${dist}".`);
}
/**
*
* @param {ReturnType<typeof parse>} ast
* @returns {Array<{ code: string; name: string; }>}
*/
function getFunctionsToTest(ast) {
/** @type {Array<{ node: object; }>} */
const functions = [];
let hasTag = false;
traverse(ast, {
FunctionDeclaration: function (path) {
functions.push({ node: path.node });
if (path.node.leadingComments?.[0].value.includes(TAG)) {
hasTag = true;
}
},
});
if (!hasTag) {
return [];
}
return functions.map(({ node }) => {
const output = generate(node);
return {
code: output.code,
name: node.id.name,
};
});
}
function debug(...msg) {
if ('DEBUG' in process.env) console.log('[DEBUG]', ...msg);
}
function info(...msg) {
console.info('[INFO]', ...msg);
}
Improvements or TODO
- rust 来写 compile
- 仅导出被单测函数使用到的函数