测试用例设计中的契诃夫之枪原则
看到一篇跟用例数据准备相关的文章,觉得挺有道理的。我之前在设计用例数据的时候就会犯类似的错误,这篇文章其实说的非常在理。
翻译了一下原文,供大家参考。
编写测试用例更像是讲故事而非纯技术工作,这种观点并不罕见。最近我在 The Bike Shed 播客中听到这个观点,而且在博客文章和会议演讲中也经常能看到类似的讨论。既然测试编写是一种讲故事的艺术,那么我们是否应该借鉴叙事原则来改进我们的测试呢?
谈到讲故事的原则,首先想到的就是契诃夫之枪原则。这个原则是什么?用安东·契诃夫自己的话说(引自大英百科全书):
如果在舞台上放置了一支上膛的步枪,那么它就必须被开火。不要做出你不打算履行的承诺。
大英百科全书还给出了如下定义:
这是一条适用于戏剧、文学和其他叙事形式的原则,它强调故事中引入的每个元素都应该对情节发展必不可少。
那么这个原则如何应用到我们的测试编写中呢?想象一下你正在为电商系统编写测试用例。你有不同类别的产品,这些类别对买家有一些限制条件。最明显的例子就是不能向未满十八岁的人销售酒类产品。在我们的例子中,这类人群不能将这些产品添加到购物车。
让我们看一个测试代码示例(我使用的是 Elixir,但这个原则适用于大多数编程语言):
test "don't allow adding products to cart when age constraint is not met" do
buyer = %Person{
name: "John Smith",
age: 17,
country: :uk,
registered_on: ~U[2023-09-16T18:17:22Z]
}
category = %Category{
name: "Alcohol",
external_id: 3242,
constraints: [
%AgeConstraint{min: 18}
]
}
product = %Product{
name: "Triple Hazy IPA",
category: category,
sku: "TRI-557",
added_at: ~U[2022-01-01T12:16:54Z]
}
cart = Cart.init(buyer)
assert Cart.add(cart, product, quantity: 2) == {:error, :constraint_violated}
end
这个测试本身并不算糟糕,但它在多处违反了契诃夫之枪原则。在我们测试的”准备”阶段引入的每个标量都是契诃夫意义上的”枪”。它们都被放在了舞台上,读者可能会期待它们都会”开火”。这里我们有 10 个标量,相当于舞台上放了 11 把枪。什么是”开火”?就是当我们把这个值改成其他值时,测试应该失败。让我们检查一下这些标量:
name: "John Smith"
: 无论改成什么,测试都不会失败age: 17
: 如果改成 18 或 22,测试会失败country: :uk
: 不会失败(除非我们实现了基于国家的限制,但目前没有)registered_on: <date>
: 无关紧要name: "Alcohol"
: 无关紧要external_id: 3242
: 这是什么?无关紧要min: 18
: 改成 15 会导致测试失败- 产品中剩余的 name、sku 和添加日期都不会影响测试
quantity: 2
: 同样无关紧要
总结一下,我们的 11 个”枪”中只有两个会”开火”,约 18%。其余的都是纯粹的干扰,如果读者试图理解测试的动态性,这些都会让他们误入歧途。
如何改进呢?通过使用抽象!测试和常规代码一样需要抽象。而且就像常规代码一样,你需要确保在特定上下文中使用正确的抽象。这里一个潜在的抽象就是工厂方法。让我们看看改进后的版本:
test "don't allow adding products to cart when age constraint is not met" do
buyer = person_factory(age: 17)
category = category_factory(constraints: [%AgeConstraint{min: 18}])
product = product_factory(category: category)
cart = Cart.init(buyer)
assert Cart.add(cart, product) == {:error, :constraint_violated}
end
测试明显变短了,只有 5 行非空代码。这里的每个标量都很重要,而且只有两个。这样我们就不会让读者负担过重,他们能够快速得出结论:啊,买家 17 岁,但限制要求至少 18 岁,所以返回了 :constraint_violated
错误。这很合理。
推论:让你的”枪”显而易见
这是我对叙事原则的补充:
如果某些东西要”开火”,要提前展示它。
人们通常不喜欢”机械降神”式的剧情。如果某个东西对测试通过很重要,就要明确地展示它。不要把它藏在抽象之下。
想象一下,在上面的测试中,第一行只是写着 buyer = person_factory()
。在代码审查时被问到这一点,开发者说工厂中的默认年龄实际上是 17,所以没必要重复。这是对抽象的误用。不要依赖于隐藏的内容。理论上来说,任何人都应该能够进入工厂修改这些值 - 而测试应该仍然通过。有些人甚至认为工厂中的默认值应该是随机的。
总结
要让你的测试用例更好、更易读(更好地讲述故事),就要删除所有与测试流程无关的数据。只保留重要的内容 - 那些改变后会导致测试失败的值。另一方面,不要在抽象中隐藏重要的内容。让叙事流程清晰明了,不要让读者感到困惑。