正在检测';死亡';测试和硬编码数据与受约束的非确定性
本文关键字:数据 受约束 编码 确定性 非确定 检测 死亡 测试 | 更新日期: 2023-09-27 18:27:57
对于那些不确定"受限非决定论"是什么意思的人,我推荐Mark Seeman的帖子。
这个想法的本质是测试只对影响SUT行为的数据具有确定性值。非"相关"数据可能在某种程度上是"随机"的。
我喜欢这种方法。数据越抽象,期望就越清晰和表达,事实上,无意识地将数据适合测试变得越来越困难。
我正试图将这种方法(以及AutoFixture)"推销"给我的同事,昨天我们对此进行了长时间的辩论。
他们提出了关于随机数据导致难以调试的测试不稳定的有趣论点
起初,这似乎有点奇怪,因为我们都同意,影响数据流的数据不能是随机的,这种行为是不可能的。尽管如此,我还是休息了一下,仔细思考了一下这个问题。最后我遇到了以下问题:
但我的一些假设首先是:
- 测试代码必须被视为生产代码
- 测试代码必须表达对系统行为的正确期望和规范
- 没有什么比损坏的构建更能警告您不一致的了(要么未编译,要么只是测试失败-门控签入)
考虑同一测试的这两种变体:
[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特有的问题。