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