第九章 - 代码生成的单元测试

89 阅读5分钟

第九章 - 代码生成的单元测试

编写代码生成脚本测试的策略与技巧

到目前为止,我们已经学习了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个数字的数组,分别表示XYZ坐标。

现在我们知道了配置的结构,下面创建一个用于单元测试的示例配置:

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"

describeitexpect是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类型、示例configcreateLine函数也放在测试文件中。通常这些应该定义在单独的文件中并导入,但为了简单起见,这里都放在一个文件中。

现在编写测试代码。我们将为所有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代码。这些知识对于团队协作和处理大型代码生成脚本非常有用。