如何对代码生成器进行单元测试?

29

我知道这是一个困难而又开放的问题,但我想把它抛出来,看看是否有任何有趣的建议。

我开发了一个代码生成器,它获取我们通过SWIG生成的Python界面到C++代码,并生成将其公开为WebServices所需的代码。当我开发这段代码时,我使用TDD进行了编写,但我发现我的测试非常脆弱。每个测试基本上都想验证对于给定的输入代码(它是一个C++头文件),我将得到特定的输出代码,因此我编写了一个小引擎,从XML输入文件中读取测试定义,并从这些期望中生成测试用例。

问题在于,我非常害怕修改代码。再加上单元测试本身非常复杂且易碎。

因此,我正在尝试思考解决此问题的替代方法,我认为我可能是错误的方式来解决它。也许我需要更注重结果,即:我生成的代码实际上是否运行并执行我想要的操作,而不是代码看起来是否符合我的要求。

是否有人有类似经历,愿意分享一下吗?


我实际上也遇到了同样的问题,下面的答案都不是特别令人满意。当然,你可以针对代码生成器的每个部分进行单元测试。但问题在于如何知道生成的代码是否正确,即没有回归或类似的问题,因此如何为生成的代码编写自动化测试(无论它们被称为单元测试还是集成测试)? - James Kingsbery
1
@James:没有简单的答案,是吗……我刚刚重新阅读了这个问题和回复,所有当时遇到的问题都涌现出来了。也许我会在未来几周再试一次,因为我不断地遇到各种回归问题,而且越来越重要的是要检测这些问题。 - jkp
这是一个大规模的字符串比较。使用AST可能会更容易。 - Nikos
8个回答

14

我开始总结我使用自己的代码生成器的经验,然后回过头来重新阅读了你的问题,发现你已经谈到了同样的问题,重点关注执行结果而不是代码布局/外观。

问题是,这很难测试,生成的代码可能不适合在单元测试系统的环境中运行,那么如何编码预期的结果?

我发现需要将代码生成器拆分成较小的部分并对其进行单元测试。如果你问我,单元测试完整的代码生成器更像集成测试而不是单元测试。


2
是的,这就像将一个大字符串与另一个进行比较,需要处理诸如:期望值等于**"{"options":但实际接收到的是{"options":**。 - Nikos

5

需要注意的是,“单元测试”只是测试的一种形式。你应该能够对代码生成器的内部模块进行单元测试。在这里,你真正关注的是系统级别测试(也称为回归测试)。这不仅涉及语义...存在不同的心态、方法、期望等等。虽然这需要更多的工作,但你可能需要咬紧牙关,建立一个端到端的回归测试套件:固定的C++文件-> SWIG接口-> Python模块->已知输出。你真的想要检查已知输入(固定的C++代码)与预期输出(最终Python程序的输出)之间的匹配情况。直接检查代码生成器的结果就像比较目标文件一样...


0

单元测试就是测试特定的单元。因此,如果您正在为A类编写规范,那么最好A类不具有B类和C类的真实具体版本。

好的,我之后注意到这个问题的标签包括了C++ / Python,但原则是相同的:

    public class A : InterfaceA 
    {   
      InterfaceB b;

      InterfaceC c;

      public A(InterfaceB b, InterfaceC c)   {
          this._b = b;
          this._c = c;   }

      public string SomeOperation(string input)   
      {
          return this._b.SomeOtherOperation(input) 
               + this._c.EvenAnotherOperation(input); 
      } 
    }

由于上述系统A向系统B和C注入接口,因此您可以对仅系统A进行单元测试,而无需让任何其他系统执行实际功能。这就是单元测试。
以下是一种聪明的方法,从创建到完成逐步处理系统,并为每个行为片段提供不同的When规范:
public class When_system_A_has_some_operation_called_with_valid_input : SystemASpecification
{
    private string _actualString;

    private string _expectedString;

    private string _input;

    private string _returnB;

    private string _returnC;

    [It]
    public void Should_return_the_expected_string()
    {
        _actualString.Should().Be.EqualTo(this._expectedString);
    }

    public override void GivenThat()
    {
        var randomGenerator = new RandomGenerator();
        this._input = randomGenerator.Generate<string>();
        this._returnB = randomGenerator.Generate<string>();
        this._returnC = randomGenerator.Generate<string>();

        Dep<InterfaceB>().Stub(b => b.SomeOtherOperation(_input))
                         .Return(this._returnB);
        Dep<InterfaceC>().Stub(c => c.EvenAnotherOperation(_input))
                         .Return(this._returnC);

        this._expectedString = this._returnB + this._returnC;
    }

    public override void WhenIRun()
    {
        this._actualString = Sut.SomeOperation(this._input);
    }
}

总之,一个单元/规范可以有多个行为,随着单元/系统的开发,规范也会增长;如果您的测试系统依赖于其中的其他具体系统,请注意。

0

我想指出的是,您仍然可以在验证结果的同时实现细粒度测试:通过将代码块嵌套在一些设置和验证代码中,您可以测试单个代码块:

int x = 0;
GENERATED_CODE
assert(x == 100);

如果您已经从较小的块组装了生成的代码,并且这些块不经常更改,那么您可以执行更多条件并进行更好的测试,希望在更改一个块的具体内容时避免所有测试都失败。


0

我的建议是找出一组已知的输入输出结果,例如您已经拥有的一些简单情况,并对生成的代码进行单元测试。当您更改生成器时,生成的确切字符串可能会略有不同,但您真正关心的是它是否以相同的方式被解释。因此,如果您像测试自己的功能代码一样测试结果,您将发现它是否按照您想要的方式成功。

基本上,您真正想知道的是,您的生成器是否会产生您期望的结果,而无需物理测试每种可能的组合(也即不可能)。通过确保您的生成器在您期望的方面保持一致,您可以更好地确信生成器将在越来越复杂的情况下成功。

通过这种方式,您还可以建立一套回归测试套件(需要继续正确工作的单元测试)。这将帮助您确保对生成器的更改不会破坏其他形式的代码。当您遇到单元测试未捕获的错误时,您可能希望将其包含在内,以防止类似的故障。


0

我发现你需要测试你生成的东西,而不是如何生成它。

在我的情况下,程序生成许多类型的代码(C#,HTML,SCSS,JS等),编译成Web应用程序。我发现减少回归错误的最佳方法是测试Web应用程序本身,而不是测试生成器。

别误会,我们仍然有单元测试来检查一些生成器代码,但我们最大的收益是对生成的应用程序进行UI测试。

由于我们正在生成它,因此我们还生成了一个漂亮的JS抽象层,可以用来编程性地测试应用程序。我们遵循了这里概述的一些想法:http://code.tutsplus.com/articles/maintainable-automated-ui-tests--net-35089

伟大的部分是它真正地从代码生成到实际生成的内容端对端地测试您的系统。一旦测试失败,很容易追踪到生成器出错的位置。

这相当棒。

祝你好运!


0

没错,结果是唯一重要的事情。真正的挑战在于编写一个框架,使您生成的代码能够独立运行...花费时间就在这里。


0

如果您正在运行*nux操作系统,您可以考虑放弃使用unittest框架,转而使用Bash脚本或Makefile。在Windows上,您可以考虑构建一个运行生成器的Shell应用/函数,然后使用代码(作为另一个进程)和unittest进行测试。

第三个选项是生成代码,然后从中构建一个仅包含unittest的应用程序。同样,您需要一个shell脚本或其他程序来为每个输入运行它。关于如何编码预期行为,我认为可以采用与C++代码相同的方式来完成,只需使用生成的接口而不是C++接口即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接