正在检测';死亡';测试和硬编码数据与受约束的非确定性

本文关键字:数据 受约束 编码 确定性 非确定 检测 死亡 测试 | 更新日期: 2023-09-27 18:27:57

对于那些不确定"受限非决定论"是什么意思的人,我推荐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.");
}

到目前为止,上帝,一切都很好,生活也很美好,但后来需求发生了变化,DoSomething改变了它的行为:现在它只在低于10的情况下增加输入,否则乘以10。这里发生了什么?带有硬编码数据的测试通过了(实际上是意外的),而第二个测试有时会失败。它们都是错误的欺骗测试:它们检查不存在的行为。

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

所以问题是:

有人建议如何以不出现这种情况的方式编写测试吗

正在检测';死亡';测试和硬编码数据与受约束的非确定性

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

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

如果你这样做不是更容易吗:

  • 首先更改expectedOutput,以满足新的要求
  • 观察失败的测试——看到它失败是很重要的
  • 只有这样才能根据新的要求修改DoSomething——使测试再次通过

这种方法与AutoFixture这样的特定工具无关,它只是测试驱动开发。


AutoFixture在哪里真的有用?使用AutoFixture,可以最小化测试的"排列"部分。

这是原始测试,使用AutoFixture编写

[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之外,还支持NUnit2。

HTH

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

真的吗?如果DoSomething改变行为,则违反打开/关闭原则(OCP)。你可能决定不关心这一点,但这与我们为什么信任测试密切相关。

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

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

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