C# 检测';死亡';测试和硬编码数据与约束非确定性

C# 检测';死亡';测试和硬编码数据与约束非确定性,c#,tdd,autofixture,C#,Tdd,Autofixture,对于那些不确定“受约束的非决定论”是什么意思的人,我推荐马克·西曼的 这个想法的本质是,测试只对影响SUT行为的数据具有确定性值。非“相关”数据在某种程度上可能是“随机”的 我喜欢这种方法。数据越抽象,期望就越清晰、越有表现力,事实上,在无意识的情况下使数据符合测试就越困难。 我正试图向我的同事“推销”这种方法,昨天我们对此进行了长时间的辩论。 他们提出了一个有趣的论点:由于随机数据,测试不稳定,调试困难。 起初,这似乎有点奇怪,因为我们都同意,影响数据流的数据不能是随机的,这样的行为是不可

对于那些不确定“受约束的非决定论”是什么意思的人,我推荐马克·西曼的

这个想法的本质是,测试只对影响SUT行为的数据具有确定性值。非“相关”数据在某种程度上可能是“随机”的

我喜欢这种方法。数据越抽象,期望就越清晰、越有表现力,事实上,在无意识的情况下使数据符合测试就越困难。

我正试图向我的同事“推销”这种方法,昨天我们对此进行了长时间的辩论。
他们提出了一个有趣的论点:由于随机数据,测试不稳定,调试困难。
起初,这似乎有点奇怪,因为我们都同意,影响数据流的数据不能是随机的,这样的行为是不可能的。尽管如此,我还是休息了一下,仔细思考了这个问题。 最后我遇到了以下问题:

但首先我的一些假设:

  • 测试代码必须视为生产代码
  • 测试代码必须表达系统行为的正确期望和规范
  • 没有什么比构建中断更能警告您不一致性了 (未编译或只是测试失败-门控签入)
  • 考虑相同测试的这两种变体:

    [TestMethod]
    public void DoSomethig_RetunrsValueIncreasedByTen()
    {
        // Arrange
        ver input = 1;
        ver expectedOutput = input+10;
    
        var sut = new MyClass();
    
        // Act
        var actualOuptut = sut.DoeSomething(input);
    
        // Assert
        Assert.AreEqual(expectedOutput,actualOutput,"Unexpected return value.");
    }
    
    /// Here nothing is changed besides input now is random.
    [TestMethod]
    public void DoSomethig_RetunrsValueIncreasedByTen()
    {
        // Arrange
        var fixture = new Fixture();
        ver input = fixture.Create<int>();
        ver expectedOutput = input+10;
    
        var sut = new MyClass();
    
        // Act
        var actualOuptut = sut.DoeSomething(input);
    
        // Assert
        Assert.AreEqual(expectedOutput,actualOutput,"Unexpected return value.");
    }
    
    [TestMethod]
    公共无效剂量值增加的字节()
    {
    //安排
    ver输入=1;
    预期输出电压=输入+10;
    var sut=新的MyClass();
    //表演
    var actualuptut=sut.DoeSomething(输入);
    //断言
    AreEqual(expectedOutput,actualOutput,“意外返回值”);
    }
    ///这里除了输入是随机的外,没有任何改变。
    [测试方法]
    公共无效剂量值增加的字节()
    {
    //安排
    var fixture=新fixture();
    ver输入=fixture.Create();
    预期输出电压=输入+10;
    var sut=新的MyClass();
    //表演
    var actualuptut=sut.DoeSomething(输入);
    //断言
    AreEqual(expectedOutput,actualOutput,“意外返回值”);
    }
    
    到目前为止,上帝,一切都在运转,生活都是美好的,但随后需求发生了变化,做一些事情也改变了它的行为:现在只有当它小于10时,它才会增加投入,否则它就会乘以10。 这里发生了什么?硬编码数据的测试通过(实际上是偶然的),而第二个测试有时失败。它们都是错误的欺骗测试:它们检查不存在的行为

    看起来不管数据是硬编码的还是随机的,都无关紧要:只是无关紧要。然而,我们没有可靠的方法来检测这种“死”测试

    因此,问题是:


    有没有什么好的建议,如何以这种情况不出现的方式编写测试?

    答案实际上隐藏在这句话中:

    […]然后需求改变,剂量测量改变其行为[…]

    如果你这样做会不会更容易:

    • 首先更改
      预期输出
      ,以满足新的要求
    • 观察失败的测试,看到它失败是很重要的
    • 只有根据新的要求修改
      DoSomething
      ,才能使测试再次通过
    此方法与AutoFixture之类的特定工具无关,它只是测试驱动的开发


    那么AutoFixture在哪里真正有用呢?使用AutoFixture,可以最小化测试的排列部分

    以下是原始测试,使用以下惯用方法编写:

    [理论,InlineAutoData]
    公共无效数据量输入超过10返回正确结果(
    我的类sut,
    [范围(int.MinValue,9)]int输入)
    {
    Assert.True(输入<10);
    预期风险值=输入+1;
    var实际值=总剂量(输入);
    断言。相等(预期、实际);
    }
    [理论,InlineAutoData]
    公共无效DoSomethingInputSequals或创建10ReturnsCorrectResult时(
    我的类sut,
    [范围(10,int.MaxValue)]int输入)
    {
    Assert.True(输入>=10);
    预期var=输入*10;
    var实际值=总剂量(输入);
    断言。相等(预期、实际);
    }
    
    另外,除了xUnit.net,还有

    “然后需求会改变,而DoSomething会改变其行为”

    真的吗?如果
    DoSomething
    改变了行为,则违反了(OCP)。你可能决定不关心这件事,但它与你的生活息息相关

    每次更改现有测试时,都会降低它们的可信度。每次更改现有生产行为时,都需要检查所有涉及该生产代码的测试。理想情况下,您需要访问每个这样的测试,并简要地更改实现,以确保在实现错误时仍然失败

    对于小的变化,这可能仍然是可行的,但是对于中等的变化,坚持OCP会更明智:不要改变现有的行为;同时添加新行为,让旧行为萎缩


    在上面的示例中,很明显AutoFixture测试可能是非决定性错误,但在更概念化的层面上,完全有可能,如果您在不检查测试的情况下更改生产行为,某些测试可能会悄悄地变成错误。这是一个与单元测试相关的一般问题,而不是特定于AutoFixture。

    好的,这里有一个问题。当分支条件变得非常复杂时,通常会创建几个测试-每个条件分支都有一个测试。您可以为新的分支编写测试,但是忘记旧的方法我非常同意您的观点,但是我认为最好在测试数据生成器中表达工作流期望。最初,它将通过方法“GetSomeInput”返回数据。稍后,当行为发生变化时,我们将删除该方法并添加另外两个“GetLessThan10”和“GetEqualOrgCreateThan10”。删除第一个方法会破坏初始测试:它不会编译@
    [Theory, InlineAutoData]
    public void DoSomethingWhenInputIsLowerThan10ReturnsCorrectResult(
        MyClass sut,
        [Range(int.MinValue, 9)]int input)
    {
        Assert.True(input < 10);
        var expected = input + 1;
    
        var actual = sut.DoSomething(input);
    
        Assert.Equal(expected, actual);
    }
    
    [Theory, InlineAutoData]
    public void DoSomethingWhenInputIsEqualsOrGreaterThan10ReturnsCorrectResult(
        MyClass sut,
        [Range(10, int.MaxValue)]int input)
    {
        Assert.True(input >= 10);
        var expected = input * 10;
    
        var actual = sut.DoSomething(input);
    
        Assert.Equal(expected, actual);
    }