检测“死”测试和硬编码数据与受约束的非确定性

时间:2014-10-25 12:52:20

标签: c# tdd autofixture

对于那些不确定“受限制的非决定论”的含义的人,我推荐Mark Seeman的post

该想法的本质是测试仅具有影响SUT行为的数据的确定性值。不“相关”的数据在某种程度上可以是“随机的”。

我喜欢这种方法。抽象的数据越多,预期就会变得越清晰和富有表现力,实际上,无意识地将数据纳入测试变得更加困难。

我正试图将这种方法(以及AutoFixture)“推销”给我的同事,昨天我们就此进行了长时间的讨论。
他们提出了一个有趣的论点,即由于随机数据而不能稳定地进行调试 起初看起来有点奇怪,因为我们都同意影响数据的流量不能是随机的,这种行为是不可能的。尽管如此,我还是休息一下,彻底思考这个问题。 我终于遇到了以下问题:

但我的一些假设首先是:

  1. 测试代码必须视为生产代码。
  2. 测试代码必须表达正确的期望和系统行为规范。
  3. 没有什么能比破坏的构建更好地警告你的不一致 (要么没有编译,要么只是失败的测试 - 门控办理登机手续)。
  4. 考虑同一测试的这两个变体:

    [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.");
    }
    

    到目前为止,上帝,一切正常,生活是美好的,然后需求发生变化,DoSomething改变了它的行为:现在只有当它低于10时才增加输入,否则增加10。 这里发生了什么?使用硬编码数据的测试通过(实际上是意外),而第二次测试有时会失败。而且他们都是错误的欺骗测试:他们检查不存在的行为。

    无论数据是硬编码还是随机数据都无关紧要:它只是无关紧要。然而,我们没有强有力的方法来检测这种“死”的测试。

    所以问题是:

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

2 个答案:

答案 0 :(得分:6)

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

  

[..]然后需求变更,DoSomething改变其行为[..]

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

  • 首先更改expectedOutput,以满足新要求。
  • 观察失败的测试 - 看它失败很重要。
  • 只有根据新要求修改DoSomething - 让测试再次通过。

此方法与AutoFixture等特定工具无关,它只是测试驱动开发。


AutoFixture在哪里真的有用吗?使用AutoFixture,您可以最小化测试的编配部分。

这是原始测试,使用AutoFixture.Xunit

以惯用方式编写
[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);
}

此外,除xUnit.net外,还有support for NUnit 2

HTH

答案 1 :(得分:5)

  

&#34;然后需求发生变化,DoSomething改变其行为&#34;

是吗,真的吗?如果DoSomething更改了行为,则会违反Open/Closed Principle(OCP)。您可能决定不关心这一点,但它与why we trust tests密切相关。

每次更改现有测试时,都会降低其可信度。每次更改现有生产行为时,您都需要审核所有触及该生产代码的测试。理想情况下,您需要访问每个此类测试,并简要地更改实现,以确定如果实现错误,它仍然会失败。

对于微小的变化,这可能仍然是实用的,但即使是适度的变化,也要明智地遵守OCP:不要改变现有的行为; 并排添加新行为,让旧行为萎缩。

在上面的例子中,可能很清楚AutoFixture测试可能是非确定性的错误,但从更概念的角度来看,如果你在不审查测试的情况下改变生产行为,那么完全有可能可能会默默地变成false negatives。这是与单元测试相关的一般问题,并非特定于AutoFixture。