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

时间:2020-03-05 18:39:38  来源:igfitidea点击:

我知道这是一个困难且开放性的问题,但我想我会把它扔到地板上,看看是否有人有任何有趣的建议。

我已经开发了一个代码生成器,它将我们的python接口与我们的C ++代码(通过SWIG生成)并生成将其公开为WebServices所需的代码。当我开发此代码时,我是使用TDD来完成的,但是我发现自己的测试非常脆弱。因为每个测试本质上都是想验证给定的输入代码(恰好是C ++头),所以我会得到给定的输出代码,所以我编写了一个小型引擎,该引擎从XML输入文件读取测试定义并生成测试来自这些期望的案例。

问题是我根本不想修改代码。单元测试本身的事实和事实是:复杂:a;脆弱。

因此,我正在尝试解决该问题的替代方法,这让我感到震惊,我可能正在以错误的方式解决它。也许我需要更多地关注结果,即IE:我生成的代码是否实际运行并按照我希望的方式运行,而不是使代码看起来像我想要的那样。

有没有人愿意分享与之类似的任何经历?

解决方案

回答

我开始编写自己的代码生成器的经验总结,然后回过头来重新阅读问题,发现我们自己已经碰到了相同的问题,专注于执行结果而不是代码布局/外观。

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

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

回答

是的,结果才是最重要的。真正繁琐的事情是编写一个框架,该框架允许我们生成的代码独立运行...花时间在那里。

回答

如果我们在* nux上运行,则可以考虑转储unittest框架,而使用bash脚本或者makefile。在Windows上,我们可以考虑构建一个运行生成器的Shell应用程序/功能,然后使用该代码(作为另一个进程)并对该单元进行测试。

第三种选择是生成代码,然后从中构建仅包含单元测试的应用程序。同样,我们将需要一个shell脚本或者其他什么来为每个输入运行此脚本。关于如何对预期行为进行编码,我发现可以使用与生成C ++代码非常相似的方式来完成编码,只是使用生成的接口而不是C ++接口。

回答

回想一下"单元测试"仅是一种测试。我们应该能够对代码生成器的内部部分进行单元测试。我们真正在这里看到的是系统级别的测试(也称为回归测试)。这不仅是语义……还有不同的思维方式,方法,期望等。当然还有更多工作要做,但是我们可能需要硬着头皮,建立一个端到端的回归测试套件:固定的C ++文件-> SWIG接口-> python模块->已知输出。我们确实想对照预期的输出(最终的Python程序产生的结果)检查已知的输入(固定的C ++代码)。直接检查代码生成器的结果就像是比较目标文件...

回答

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

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

如果我们将生成的代码由较小的块组装而成,并且这些块不会经常更改,则我们可以行使更多条件并进行更好的测试,并希望避免在更改一个块的细节时所有测试都中断。

回答

单元测试只是测试特定的单元。因此,如果我们正在编写有关类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);
    }
}

因此,总而言之,单个单元/规范可以具有多种行为,并且随着开发单元/系统的规范不断增长。如果被测系统依赖于其中的其他具体系统,请当心。

回答

我的建议是找出一组已知的输入输出结果,例如一些已经存在的简单案例,并对所生成的代码进行单元测试。完全有可能,当我们更改生成器时,所生成的确切字符串可能会稍有不同……但是我们真正关心的是,是否以相同的方式解释它。因此,如果我们测试结果就像测试该代码是否是功能,我们将发现它是否以我们想要的方式成功。

基本上,我们真正想知道的是,如果不对每个可能的组合进行物理测试(也:不可能),生成器是否会产生我们期望的结果。通过确保生成器与我们期望的方式保持一致,我们可以更好地感到生成器在越来越复杂的情况下能够成功。

这样,我们还可以建立一套回归测试(需要保持正常工作的单元测试)。这将确保对生成器所做的更改不会破坏其他形式的代码。当遇到单元测试未捕获的错误时,我们可能希望将其包括在内以防止类似的损坏。