第九章 - 代码生成的单元测试
编写代码生成脚本测试的策略与技巧
到目前为止,我们已经学习了TypeScript编译器API、抽象语法树(AST)和代码生成的相关知识。现在,我们要学习如何通过编写配套的单元测试来构建可靠的脚本。这对于开发团队尤为重要,因为其他开发者可能需要修改脚本,同时仍需确保所有功能按预期工作。
本章我们将编写一个新的代码生成脚本。该脚本会根据配置生成ThreeJS代码,并使用Vitest编写单元测试来验证脚本的正确性。
如果你不熟悉这些工具也没关系。ThreeJS是一个使用浏览器canvas元素创建3D图形的JavaScript/TypeScript库,功能非常强大。Vitest则是一个测试运行工具,提供实用函数和开发服务器来简化测试流程,其功能与Jest非常相似。
生成彩色3D线条
在这个示例中,我们将定义一个配置对象,其中包含生成ThreeJS代码所需的数据。这个配置会记录材质颜色和若干Vector3点。其TypeScript类型定义如下:
type Config = {
materialColor: number
points: [number, number, number][]
}
假设这个配置由用户或数据库提供。我们将基于此配置生成一个非常基础的几何体——彩色线条!线条将使用单一颜色,并允许用户通过materialColor字段指定颜色,通过points数组指定任意数量的点。每个点都是一个包含3个数字的数组,分别表示X、Y和Z坐标。
现在我们知道了配置的结构,下面创建一个用于单元测试的示例配置:
const config: Config = {
materialColor: 0x0000ff,
points: [
[10, 0, 0],
[0, 10, 0],
[0, 0, 0]
]
}
这是一个简单的配置,使用十六进制颜色值0x0000ff表示蓝色,并定义了3个点。
对于这个配置,我们期望生成的代码如下:
const points = [new THREE.Vector3(10, 0, 0), new THREE.Vector3(0, 10, 0), new THREE.Vector3(10, 0, 0)]
const geometry = new THREE.BufferGeometry().setFromPoints(points)
const material = new THREE.LineBasicMaterial({ color: 0x0000ff })
const line = new THREE.Line(geometry, material)
即使你不理解这段代码也没关系。本章的重点不是学习ThreeJS,而是学习如何测试代码生成脚本。
根据第7章学到的知识,我们将预期输出粘贴到ts-ast-viewer中,复制factory代码作为脚本基础。然后将其封装在createLine(config)函数中,并修改factory代码使其能根据配置动态设置材质颜色和线条。以下是最终用于单元测试的代码生成片段:
function createLine(config: Config): ts.NodeArray<ts.Statement> {
function createVector3(point: [number, number, number]) {
return ts.factory.createNewExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("THREE"),
ts.factory.createIdentifier("Vector3")
),
undefined,
[
ts.factory.createNumericLiteral(point[0]),
ts.factory.createNumericLiteral(point[1]),
ts.factory.createNumericLiteral(point[2])
]
)
}
const pointsVectors = config.points.map(createVector3)
const statements = [
ts.factory.createVariableStatement(
undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
ts.factory.createIdentifier("points"),
undefined,
undefined,
ts.factory.createArrayLiteralExpression(pointsVectors, true)
)
],
ts.NodeFlags.Const
)
),
// 其余代码保持不变...
]
return ts.factory.createNodeArray(statements)
}
由于第7章已经介绍了factory代码的工作原理,这里不再逐行解释。我们可以假设上述代码能根据配置生成预期输出。
但真的能假设吗?还是写个单元测试来验证吧!
编写测试
首先通过npm install vitest安装vitest,然后创建lines.spec.ts文件。在package.json中添加test脚本,其值为vitest,这样就能通过npm run test运行测试。
打开lines.spec.ts,先粘贴以下基础代码:
import { describe, it, expect } from "vitest"
import ts from "typescript"
describe、it和expect是Vitest提供的辅助函数,用于组织测试代码。
测试代码生成脚本时,最好有一个工具函数将factory代码编译为字符串输出。这样可以更方便地测试factory方法的输出是否符合预期。这个工具函数如下:
function compile(nodes: ts.NodeArray<ts.Statement>) {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed })
const resultFile = ts.createSourceFile("temp.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX)
return printer.printList(ts.ListFormat.MultiLine, nodes, resultFile)
}
compile函数接收一个NodeArray语句数组,并将其转换为多行字符串以便测试。
为了方便测试,可以将Config类型、示例config和createLine函数也放在测试文件中。通常这些应该定义在单独的文件中并导入,但为了简单起见,这里都放在一个文件中。
现在编写测试代码。我们将为所有ThreeJS生成代码创建一个describe块,再为线条生成函数创建一个it块。在it块中调用createLine(config)函数并传入示例配置,然后将输出传给compile函数转换为字符串,最后用expect进行断言。代码如下:
describe("Generates ThreeJS Code", () => {
it("Generates Line", () => {
expect(compile(createLine(config))).toEqual(``)
})
})
运行npm run test,测试应该会失败,因为我们将.toEqual('')设为了空字符串。现在将预期输出粘贴进去重新运行测试:
describe("Generates ThreeJS Code", () => {
it("Generates Line", () => {
expect(compile(createLine(config))).toEqual(`const points = [
new THREE.Vector3(10, 0, 0),
new THREE.Vector3(0, 10, 0),
new THREE.Vector3(0, 0, 0)
];
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 255 });
const line = new THREE.Line(geometry, material);
`)
})
})
现在测试应该通过了!如果失败,请检查错误信息,很可能是空格或换行符导致的。格式必须完全一致。
本章小结
本章我们学习了如何为factory代码编写单元测试!我们创建了一个工具函数,用于快速将factory代码编译为字符串,从而能够独立测试factory代码。这些知识对于团队协作和处理大型代码生成脚本非常有用。