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

Pav*_*nin 5 c# tdd autofixture

对于那些不确定"受限制的非决定论"是什么意思的人,我推荐Mark Seeman的帖子.

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

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

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

但我的一些假设首先是:

  1. 测试代码必须被视为生产代码.
  2. 测试代码必须表达正确的期望和系统行为规范.
  3. 没有什么能比破坏的构建更好地警告你不一致(要么没有编译,要么只是失败的测试 - 门控签到).

考虑同一测试的这两个变体:

[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.");
}
Run Code Online (Sandbox Code Playgroud)

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

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

所以问题是:

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

Nik*_*nis 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);
}
Run Code Online (Sandbox Code Playgroud)

此外,除xUnit.net外,还支持NUnit 2.

HTH


Mar*_*ann 5

"然后需求变化,DoSomething改变其行为"

真的吗?如果DoSomething改变行为,则违反了开放/封闭原则(OCP).您可能决定不关心这一点,但它与我们信任测试的原因密切相关.

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

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

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

  • @voroninp关于固有不稳定域的问题是,这些特别是OCP有价值的域,因为OCP使您能够保持旧逻辑.这再次使您能够重新计算过去的**.它还使您能够恢复旧规则,以防任何人改变主意. (2认同)