如何单元 and/or 集成测试解析器?
How to unit and/or integration test parsers?
我想知道测试解析器是否正确解析为 AST 的一般原则是什么。我过去曾尝试测试 AST 的结构,但这有两个大问题。
- 可读性。很难阅读测试并理解它在做什么。
- 可维护性。有大量的测试需要编写,因此增加了可读性。如果您完全更改解析器,则可能需要重写所有测试。
假设我们的解析器在 JavaScript 中,我们的 AST 在 JSON 中。而不是测试 JSON AST 输出结构匹配一些“预期的”JSON 对象,像这样:
assert.deepEqual(parse('x = 10'), {
type: 'assignment',
left: {
type: 'variable',
name: 'x',
},
right: {
type: 'integer',
value: 10
}
})
我会做这样的事情:
test('x = 10')
function test(str) {
let parsed = parse(str)
let generated = stringify(parsed)
assert.equal(str, generated)
}
然后它会转一圈,从字符串回到字符串,你只需要读取输入的字符串。这样做的优缺点是什么,是否有更好的标准方法?与此相关的一个缺点是,您的字符串化器必须 格式化 字符串的方式与最初写入的字符串完全相同,这可能不太理想。但是这样做的一个主要优点是你可以简单地写出你的字符串一次,并且很容易阅读你的测试。您只需确保您的 test
函数没有作弊并返回初始字符串,显然 :).
注:我写的例子x = 10
是一个极其简单的例子。为了稳健地测试解析器,我们将需要集成了所有功能的复杂完整程序,因此如果将 AST 与预期进行比较,AST 将变得更加“精神分析”以弄清楚测试的作用 JSON结构。
另请注意:这只是测试 AST 生成器的工作情况。一个单独的 编译器 最终也必须进行测试。但是这里我们不关心编译器,而是只关注解析器。
实际上,您的第一个解决方案比您的第二个想法“更好”。
但是,不要检查是否完全相等。做“包含”类型测试。结果 AST 应包含“类型:变量;名称:x”。那是一个测试。它应该包含“类型:整数;值:10”。它应该包含“left { variable }”。第三次测试。现在,所有 3 个测试都可以是一个测试的一部分,并且可以有更多的部分。但是想想你想要捕获什么语义并确保它们被捕获。
此外,将这些检查转化为断言——不仅在您的测试中,而且在您的 AST 构建器中。 AST 构建器应该断言赋值 AST 的 lhs 是可赋值的(最初只是一个变量,但随着你的松动它会更多)。
您也可以对字符串执行此操作,但实际上这更难,除非您可以控制字符串或编写自定义差异来检查它们。此外,就语义而言,您实际上并不知道字符串测试是否证明了什么。你知道你得到了相同的字符串,但这可能是因为你在读写方面有相应的缺陷。这是您实际上可能犯的错误。
最后,在我的上一个项目中,我们对很多事情使用了 json 输出,我写了一个 json 差异检查器。因此,我们可以在测试“成功”后保存 json,然后比较所有后续运行的 json 输出。是的,这是一个“变化检测器”,但是在编写编译器时,您真的想知道何时更改了某些内容的解析方式而不是意外。您无需手动检查 json 输出,除非它已更改(然后如果您对更改没问题),您只需将该文件保存为新的回归检查。而且,如果 json 的某些部分有所不同,只需让差异工具忽略这些差异(也许只是记录它们)。请参阅我的比较示例,了解您可能希望在差异工具中“匹配”哪些类型的内容。
One con with this is that your stringifier has to format the string exactly the same way as the initially written string, which might be less than ideal.
你不应该 运行 进入这个问题,如果你这样做,JSON 在这种情况下应该是稳定的重新编码。
也就是说,如果 AST 是 JSON 文本,而等式断言又是 JSON 文本,那么两个 JSON 文本都可以被解码,然后你可以做你的断言:
actualJsonText = parse('x = 10');
expectedJsonText = lastAssertedSuccess();
assert.deepEqual(decode(actualJsonText), decode(expectedJsonText))
lastAsserted(actualJsonText);
与不同场景相似,例如parse() 实际上有 AST:
actualJsonText = encode(parse('x = 10'));
expectedJsonText = lastAssertedSuccess();
assert.deepEqual(decode(actualJsonText), decode(expectedJsonText))
lastAsserted(actualJsonText);
这些可能不是最好的单元测试,因为它创建的数据库会随着时间的推移而增长。但是,它允许您测试输入的输出,如果 JSON 文本采用漂亮的打印格式,则标准文本差异将显示人类可读格式的变化。
如果测试是误报,可以使用新的好主控轻松覆盖最后一个已知的好断言。也就是说,您可以相对轻松地维护这种测试套件。
当我编写解析器或类似工具(例如标记器:二进制字符串 -> 标记流)时,我经常创建这样的已知输入 -> 预期输出测试套件,并且我想测试整体功能并有一个已知商品数据库以捕获回归。
为此,它运行良好且高效。它需要当断言无法理解原因时 - 测试不会告诉你。
这些类型的测试也不会告诉您内部结构(例如解析器状态),您可能需要预先进行更多专用测试,但这些在很大程度上取决于实现细节。并且实现可以通过测试驱动完成,所以这已经涵盖了。
我想知道测试解析器是否正确解析为 AST 的一般原则是什么。我过去曾尝试测试 AST 的结构,但这有两个大问题。
- 可读性。很难阅读测试并理解它在做什么。
- 可维护性。有大量的测试需要编写,因此增加了可读性。如果您完全更改解析器,则可能需要重写所有测试。
假设我们的解析器在 JavaScript 中,我们的 AST 在 JSON 中。而不是测试 JSON AST 输出结构匹配一些“预期的”JSON 对象,像这样:
assert.deepEqual(parse('x = 10'), {
type: 'assignment',
left: {
type: 'variable',
name: 'x',
},
right: {
type: 'integer',
value: 10
}
})
我会做这样的事情:
test('x = 10')
function test(str) {
let parsed = parse(str)
let generated = stringify(parsed)
assert.equal(str, generated)
}
然后它会转一圈,从字符串回到字符串,你只需要读取输入的字符串。这样做的优缺点是什么,是否有更好的标准方法?与此相关的一个缺点是,您的字符串化器必须 格式化 字符串的方式与最初写入的字符串完全相同,这可能不太理想。但是这样做的一个主要优点是你可以简单地写出你的字符串一次,并且很容易阅读你的测试。您只需确保您的 test
函数没有作弊并返回初始字符串,显然 :).
注:我写的例子x = 10
是一个极其简单的例子。为了稳健地测试解析器,我们将需要集成了所有功能的复杂完整程序,因此如果将 AST 与预期进行比较,AST 将变得更加“精神分析”以弄清楚测试的作用 JSON结构。
另请注意:这只是测试 AST 生成器的工作情况。一个单独的 编译器 最终也必须进行测试。但是这里我们不关心编译器,而是只关注解析器。
实际上,您的第一个解决方案比您的第二个想法“更好”。
但是,不要检查是否完全相等。做“包含”类型测试。结果 AST 应包含“类型:变量;名称:x”。那是一个测试。它应该包含“类型:整数;值:10”。它应该包含“left { variable }”。第三次测试。现在,所有 3 个测试都可以是一个测试的一部分,并且可以有更多的部分。但是想想你想要捕获什么语义并确保它们被捕获。
此外,将这些检查转化为断言——不仅在您的测试中,而且在您的 AST 构建器中。 AST 构建器应该断言赋值 AST 的 lhs 是可赋值的(最初只是一个变量,但随着你的松动它会更多)。
您也可以对字符串执行此操作,但实际上这更难,除非您可以控制字符串或编写自定义差异来检查它们。此外,就语义而言,您实际上并不知道字符串测试是否证明了什么。你知道你得到了相同的字符串,但这可能是因为你在读写方面有相应的缺陷。这是您实际上可能犯的错误。
最后,在我的上一个项目中,我们对很多事情使用了 json 输出,我写了一个 json 差异检查器。因此,我们可以在测试“成功”后保存 json,然后比较所有后续运行的 json 输出。是的,这是一个“变化检测器”,但是在编写编译器时,您真的想知道何时更改了某些内容的解析方式而不是意外。您无需手动检查 json 输出,除非它已更改(然后如果您对更改没问题),您只需将该文件保存为新的回归检查。而且,如果 json 的某些部分有所不同,只需让差异工具忽略这些差异(也许只是记录它们)。请参阅我的比较示例,了解您可能希望在差异工具中“匹配”哪些类型的内容。
One con with this is that your stringifier has to format the string exactly the same way as the initially written string, which might be less than ideal.
你不应该 运行 进入这个问题,如果你这样做,JSON 在这种情况下应该是稳定的重新编码。
也就是说,如果 AST 是 JSON 文本,而等式断言又是 JSON 文本,那么两个 JSON 文本都可以被解码,然后你可以做你的断言:
actualJsonText = parse('x = 10');
expectedJsonText = lastAssertedSuccess();
assert.deepEqual(decode(actualJsonText), decode(expectedJsonText))
lastAsserted(actualJsonText);
与不同场景相似,例如parse() 实际上有 AST:
actualJsonText = encode(parse('x = 10'));
expectedJsonText = lastAssertedSuccess();
assert.deepEqual(decode(actualJsonText), decode(expectedJsonText))
lastAsserted(actualJsonText);
这些可能不是最好的单元测试,因为它创建的数据库会随着时间的推移而增长。但是,它允许您测试输入的输出,如果 JSON 文本采用漂亮的打印格式,则标准文本差异将显示人类可读格式的变化。
如果测试是误报,可以使用新的好主控轻松覆盖最后一个已知的好断言。也就是说,您可以相对轻松地维护这种测试套件。
当我编写解析器或类似工具(例如标记器:二进制字符串 -> 标记流)时,我经常创建这样的已知输入 -> 预期输出测试套件,并且我想测试整体功能并有一个已知商品数据库以捕获回归。
为此,它运行良好且高效。它需要当断言无法理解原因时 - 测试不会告诉你。
这些类型的测试也不会告诉您内部结构(例如解析器状态),您可能需要预先进行更多专用测试,但这些在很大程度上取决于实现细节。并且实现可以通过测试驱动完成,所以这已经涵盖了。