测试生成器是一种 track 专属的软件,用于自动生成练习的测试。它的做法是把练习的 JSON 测试用例转换成该 track 所用语言编写的测试。
拥有测试生成器的一些好处是:
一般来说,运行测试生成器是为了:
为一个新练习添加测试生成器后,就可以生成它的测试文件。只要测试生成器本身已经实现,为新练习生成测试,会比从零开始手写省下(远远)更多的功夫。
练习有了测试生成器之后,你可以重新运行它,让练习与最新的规范数据保持更新/同步。我们建议定期这样做,检查有没有需要更新的问题测试用例,或者有没有你想加入的新测试。
为一个练习实现测试生成器时,有两种可能的起点:
如果已经存在测试,请让测试生成器生成的测试不要破坏已有的解答。
大体上说,测试文件有两种生成方式:
我们发现,基于代码的做法会让测试生成器的代码相当复杂,而基于模板的做法则更简单。
我们推荐的做法是:
tests.toml 文件中标记为 include = false 的测试用例这种做法的关键好处在于,每个练习都有自己的模板,这:
设计测试生成器时,请尽量:
测试生成器通常(大部分)用该 track 的语言编写。
你当然可以自由使用其他语言,但每多一种语言,都会让 track 的维护和贡献变得更困难。 因此,我们建议尽量使用该 track 的语言,因为这样维护和贡献都会更容易。
如果你的 track 有格式化代码的工具,可以考虑在渲染模板_之后_把它作为后处理步骤运行。
测试生成器处理的核心数据,是练习的 canonical-data.json 文件。这个文件定义在 exercism/problem-specifications 代码仓库中,该仓库为许多 Exercism 练习定义了共享的元数据。
并不是所有练习都有 canonical-data.json 文件!
如果没有,你就得手动创建测试,因为测试生成器没有可处理的数据。
规范数据定义在一个 JSON 对象里。这个对象包含一个 "cases" 字段,里面装着测试用例。这些测试用例(通常)与你的 track 中的测试一一对应。
每个测试用例都有若干属性,其中最重要的是 description、property、输入值和期望值。下面是 leap 练习的 canonical-data.json 文件的一个(部分)示例:
{
"exercise": "leap",
"cases": [
{
"uuid": "6466b30d-519c-438e-935d-388224ab5223",
"description": "year not divisible by 4 in common year",
"property": "leapYear",
"input": {
"year": 2015
},
"expected": false
},
{
"uuid": "4fe9b84c-8e65-489e-970b-856d60b8b78e",
"description": "year divisible by 4, not divisible by 100 in leap year",
"property": "leapYear",
"input": {
"year": 1996
},
"expected": true
}
]
}
测试生成器的主要职责,就是把这些 JSON 数据转换成 track 专属的测试。上面的 JSON 转换成 Nim 测试代码后大概是这样的:
import unittest
import leap
suite "Leap":
test "year not divisible by 4 in common year":
check isLeapYear(2015) == false
test "year divisible by 4, not divisible by 100 in leap year":
check isLeapYear(1996) == true
canonical-data.json 文件的结构有详细的文档,也有 JSON schema 定义。
有些练习的规范数据使用了嵌套。也就是说,cases 数组中的每个元素可以是:
要判断一个元素属于哪种类型,可以检查它是否存在某种类型独有的字段。
最好的做法大概是用 "cases" 键,因为只有测试用例组才会有它。
下面是一个嵌套测试用例的例子:
{
"cases": [
{
"uuid": "e9c93a78-c536-4750-a336-94583d23fafa",
"description": "data is retained",
"property": "data",
"input": {
"treeData": ["4"]
},
"expected": {
"data": "4",
"left": null,
"right": null
}
},
{
"description": "insert data at proper node",
"cases": [
{
"uuid": "7a95c9e8-69f6-476a-b0c4-4170cb3f7c91",
"description": "smaller number at left node",
"property": "data",
"input": {
"treeData": ["4", "2"]
},
"expected": {
"data": "4",
"left": {
"data": "2",
"left": null,
"right": null
},
"right": null
}
}
]
}
]
}
如果你的 track 不支持分组测试,你就需要:
cases 层级,只保留最内层(叶子)的测试用例测试用例的 input 和 expected 键内容差别很大。大多数情况下,它们是标量值(比如数字、布尔值或字符串)或简单的对象。不过偶尔也会遇到比较复杂的值,可能需要做一些预处理,比如伪代码中的 lambda、要在学员代码上执行的一组操作等等。
测试用例有一个可选的 scenarios 字段。测试生成器可以用这个字段对某些测试用例做特殊处理。最常见的用法是忽略某类测试,比如带有 "unicode" 场景的测试,因为你的 track 所用的语言可能不支持 Unicode。
完整的场景列表见这里。
读取 canonical-data.json 文件有几种办法:
problem-specifications 代码仓库获取(例如 https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/leap/canonical-data.json)。problem-specifications 代码仓库作为 Git 子模块添加到 track 代码仓库。configlet 缓存中读取。
缓存位置取决于用户的系统,但你可以用 configlet info -o -v d | head -1 | cut -d " " -f 5 以编程方式取得该位置。如果你的 track 想加入一些额外的、track 专属的测试用例(规范数据里没有的),一种办法是创建一个 additional-test-cases.json 文件,测试生成器可以把它与 canonical-data.json 文件合并,然后再交给模板渲染。
用哪种模板引擎,多半取决于具体的 track。理想情况下,你希望模板越直白越好,所以不必在意代码重复之类的问题。
模板本身的数据来自测试生成器,测试生成器会遍历模板并渲染它们。
为了帮助模板保持简单,可以在测试生成器一侧做一点预处理,或者定义一些“过滤器”,或使用你的模板所支持的任何扩展机制。
configlet 是 track 的主要维护工具,可以用它来:
bin/configlet create --practice-exercise <slug>
tests.toml 文件:运行 bin/configlet sync --tests --update --exercise <slug>
因此,把 configlet 和测试生成器结合起来使用,可以打造出一些非常强大的工作流。
你会希望测试生成器既好用_又_强大。为此,我们建议创建一个或多个脚本文件。
你可以自由选择最适合你的 track 的脚本文件格式。 shell 脚本和 PowerShell 脚本都是常见且好用的选择。
下面是一个 shell 脚本示例,它把 configlet 和测试生成器结合起来,快速搭建一个新练习:
bin/fetch-configlet
bin/configlet create --practice-exercise <slug>
path/to/test-generator <slug>
在动手构建测试生成器之前,建议你先看看几个现成的测试生成器,感受一下其他 track 是怎么实现的:
如果你有任何问题,论坛是最好的提问地方。 论坛上关于 Rust以及 JavaScript 测试生成器的讨论也许会有帮助。
我们建议逐步构建测试生成器,从最小可行产品开始。最简版本可以只是读取练习的 canonical-data.json,然后把数据直接传给模板。
先专注做好一个练习,最好是像 leap 这样简单的练习。等它跑通之后,再逐步加入更多练习。
并且尽量让测试生成器保持简单。
理想情况下,贡献者只需要粘贴/修改现有的模板,而无需理解测试生成器的内部工作原理。
如何使用或贡献测试生成器,取决于具体的 track。相关说明可以在 track 的 README.md、CONTRIBUTING.md 或测试生成器代码所在的目录里找到。