我参与了一个使用 Eclipse 编译器将 Java AST 翻译成另一种语言 OpenCL 的项目,并且遇到了类似的问题。
我对你没有神奇的解决方案,但如果有帮助,我会分享我的经验。
您使用预期输出(使用 output.txt)进行测试的技术也是我开始的方式,但它成为测试的绝对维护噩梦。当我出于某种原因(发生了几次)不得不更改生成器或输出时,我不得不重写所有预期的输出文件——而且它们的数量很大。我开始根本不想更改输出,因为害怕破坏所有测试(这很糟糕),但最后我放弃了它们,而是对生成的 AST 进行了测试。这意味着我可以“松散地”测试输出。例如,如果我想测试 if 语句的生成,我可以在生成的类中找到唯一的 if 语句(我编写了辅助方法来完成所有这些常见的 AST 工作),验证一些关于它的事情,然后做完了。那个测试不会 不在乎类是如何命名的,或者是否有额外的注释或注释。由于测试更加集中,这最终工作得很好。缺点是测试与代码的耦合更紧密,所以如果我想删除 Eclipse 编译器/AST 库并使用其他东西,我需要重写我的所有测试。最后,因为代码生成会随着时间而改变,我愿意付出这个代价。
我还严重依赖集成测试 - 以目标语言实际编译和运行生成代码的测试。与单元测试相比,我拥有更多这些类型的测试,纯粹是因为它们似乎更有用并能发现更多问题。
至于访问者测试,我再次使用它们进行更多集成式测试 - 获取一个非常小的/特定的 Java 源文件,用 Eclipse 编译器加载它,用它运行我的一个访问者并检查结果。在不调用 Eclipse 编译器的情况下进行测试的唯一另一种方法是模拟整个 AST,这是不可行的——大多数访问者都是不平凡的,并且需要一个完全构造/有效的 Java AST,因为他们会从主类中读取注释. 大多数访问者都可以通过这种方式进行测试,因为他们要么生成小的 OpenCL 代码片段,要么构建了单元测试可以验证的数据结构。
是的,我所有的测试都与 Eclipse 编译器紧密耦合。但我们正在编写的实际软件也是如此。使用其他任何东西都意味着无论如何我们都必须重写整个程序,所以我们很乐意为此付出代价。我想没有一种解决方案——你需要权衡紧耦合的成本与测试的可维护性/简单性。
我们也有相当多的测试实用程序代码,例如使用默认设置设置 Eclipse 编译器,提取方法树的主体节点的代码等。我们尽量保持测试尽可能小(我知道这是可能是常识,但可能值得一提)。
(下面对评论的编辑/添加 - 比评论回复更容易阅读/格式化)
“我还严重依赖集成测试——实际编译和运行目标语言生成代码的测试” 这些测试实际上做了什么?它们与 output.txt 测试有何不同?
(再次编辑:重新阅读问题后,我意识到我们的方法是相同的,所以忽略这个)
集成测试不仅仅是生成源代码并将其与我最初所做的预期输出进行比较,而是生成 OpenCL 代码、编译并运行它。所有生成的代码都会产生输出,然后比较该输出。
例如,我有一个 Java 类,如果生成器正常工作,它应该生成 OpenCL 代码,将两个缓冲区中的值相加,然后将值放入第三个缓冲区。最初,我会用预期的 OpenCL 代码编写一个文本文件,并在我的测试中进行比较。现在,集成测试生成代码,通过 OpenCL 编译器运行它,运行它,然后测试检查值。
“至于访问者测试,我再次与他们进行更多的集成式测试——获取一个非常小的/特定的 Java 源文件,用 Eclipse 编译器加载它,用它运行我的一个访问者并检查结果。”你的意思是运行与您的一位访问者一起,还是将所有访问者运行到您要测试的访问者?
大多数访问者可以彼此独立运行。在可能的情况下,我将只与我正在测试的访问者一起运行,或者如果依赖于其他访问者,则需要最少的访问者集(通常只需要另一个访问者)。访问者不直接相互交谈,而是使用传递的上下文对象。这些可以在测试中人为地构建,以使事物进入已知状态。
另一个问题,你在这个项目中使用模拟吗?此外,您是否经常在其他项目中使用模拟?我只是想清楚地了解我正在与之交谈的人:P
在这个项目中,我们在大约 5% 的测试中使用了模拟,甚至可能更少。而且我不会模拟任何 Eclipse 编译器的东西。
模拟的问题是我需要了解我正在模拟的内容,而 Eclipse 编译器并非如此。有很多访问者方法被调用,有时我不确定应该调用哪一个(例如,访问 ExtendedStringLiteral 或访问 StringLiteral 调用字符串文字?)如果我确实模拟了这一点并假设其中一个,这可能与现实不符,即使测试通过,程序也会失败 - 这是不希望的。我们做的唯一模拟是一对注解处理器 API、一对 Eclipse 编译器适配器和一些我们自己的核心类。
其他项目,例如 Java EE 的东西,使用了更多的模拟,但我仍然不是它们的狂热用户。API 越定义、理解和可预测,我就越有可能考虑使用模拟。
我们程序的第一阶段就像一个普通的编译器。我们从源文件中提取信息并填写一个(大而复杂的!)符号表。您将如何进行系统测试?理论上,我可以使用源文件以及包含有关 symbolTable 的所有信息的 symbolTable.txt(或 .xml 或其他)创建一个测试,但我认为这样做会有点复杂。这些集成测试中的每一个都将是一件复杂的事情!
我会尝试采用测试符号表的一小部分而不是一次性测试全部的方法。如果我正在测试是否正确构建了 Java 树,我会有类似的东西:
一项仅针对 if 语句的测试:
- 具有包含一个 if 语句的一种方法的源代码
- 从此源构建符号表/树
- 从主类中仅从方法体中提取语句树(如果 >1 或没有方法体、找到类、方法体中的顶级语句节点,则测试失败)
- 以编程方式比较 if 语句的节点属性(条件、正文)
至少以相似的风格测试每种其他类型的陈述。
- 其他测试,可能用于多个语句等或任何需要的测试
这种方法是集成式测试,但每个集成测试只测试系统的一小部分。
基本上我会尽量保持测试尽可能小。许多用于提取树的部分的测试代码可以移动到实用程序方法中,以保持测试类较小。
我想也许我可以创建一个漂亮的打印机来处理符号表并输出相应的源文件(如果一切正常,就像原始源文件一样)。问题是原始文件的顺序可能与我漂亮的打印机打印的顺序不同。恐怕用这种方法我可能只是打开另一罐蠕虫。我一直在无情地重构部分代码,并且错误开始显露出来。我真的需要一些集成测试来让我走上正轨。
这正是我所采取的方法。但是在我的系统中,东西的顺序并没有太大变化。我有生成器,它们基本上输出代码以响应 Java AST 节点,但是生成器可以递归地调用自己有一点自由。例如,响应 Java If 语句 AST 节点触发的 'if' 生成器可以写出 'if (',然后要求其他生成器呈现条件,然后写 ') {',要求其他生成器编写出正文,然后写'}'。