8

什么被认为是对复杂单元(如编译器)进行单元测试的最佳方法?

多年来,我编写了一些编译器和解释器,我确实发现这种代码很难以一种好的方式进行测试。

如果我们采用抽象语法树生成之类的东西。您将如何使用 TDD 进行测试?

小型结构可能很容易测试。例如:

string code = @"public class Foo {}";
AST ast = compiler.Parse(code);

因为这不会产生很多 ast 节点。

但是,如果我真的想测试编译器是否可以为类似方法的东西生成 AST:

[TestMethod]
public void Can_parse_integer_instance_method_in_class ()
{
   string code = @"public class Foo {  public int method(){ return 0;}}";
   AST ast = compiler.Parse(code);

你会断言什么?手动定义一个表示给定代码的 AST 并断言生成的 AST 符合手动定义的 AST 似乎非常麻烦,甚至可能容易出错。

那么对于这样的复杂场景进行 TDD 处理的最佳策略是什么?

4

2 回答 2

6

首先,如果您测试编译器,您将无法获得足够的测试!用户真的依赖编译器生成的输出,就好像它是一个永远的黄金标准,所以要真正注意质量。所以如果可以的话,用你能想到的每一个测试来测试!

其次,使用所有可用的测试方法,但在适当的时候使用它们。实际上,您也许能够在数学上证明某个变换是正确的。如果你能够这样做,你应该这样做。

但我见过的每个编译器都涉及启发式和大量优化的、手工编写的内部代码。因此,辅助证明方法通常不再适用。在这里,测试已经到位,我的意思是很多!

在收集测试时,请考虑不同的情况:

  1. 积极的标准一致性:您的前端应该接受某些代码模式,并且编译器必须生成正确运行的程序。此类测试要么需要黄金参考编译器,要么需要生成测试程序正确输出的生成器;或者它涉及手写程序,其中包括对人类推理提供的值的检查。
  2. 负面测试:每个编译器都必须拒绝错误代码,例如语法错误、类型不匹配等。它必须产生某些类型的错误和警告消息。我不知道任何自动生成此类测试的方法。所以这些也需要人工编写。
  3. 转换测试:每当您在编译器(中端)中提出花哨的优化时,您可能会想到一些演示优化的示例代码。请注意此类模块之前和之后的转换,它们可能需要为您的编译器或仅插入该模块的准系统编译器提供特殊选项。也测试一组合理的周边模块组合。我通常在特定转换之前和之后对中间表示进行回归测试,通过与同事的深入推理来定义参考。尝试在转换的两侧编写代码,即您想要转换的代码片段和不能转换的稍微不同的代码片段。

现在,这听起来像是一项非常艰巨的工作!是的,但有帮助:世界上有几个用于 (C-) 编译器的商业测试套件和专家可以帮助您应用它们。这是我所知道的一小部分:

于 2013-03-25T11:08:50.067 回答
1

首先,解析通常是编译器项目的一个微不足道的部分。根据我的经验,它永远不会花费超过 10% 的时间(除非我们谈论的是 C++,但如果你正在设计它,你就不会在这里问问题),所以你宁愿不要将大量时间投入到解析器测试上。

尽管如此,TDD(或者不管你怎么称呼它)在开发中端中占有一席之地,你经常想要验证你刚刚添加的优化是否确实导致了预期的代码转换。根据我的经验,这样的测试通常是通过为编译器提供特制的测试程序并为预期的模式搜索输出程序集来实现的(这个循环是否展开了四次?我们是否设法避免内存写入是这个函数吗?等等)。Grepping 程序集不如分析结构化表示(S-exprs 或 XML),但它便宜且在大多数情况下工作正常。但是,随着编译器的增长,很难支持它。

于 2013-03-14T16:01:21.963 回答