What Makes a Good Unit Test?说测试应该只测试一件事。有什么好处?
编写更大的测试来测试更大的代码块会不会更好?无论如何,调查测试失败很困难,我从小测试中看不到它的帮助。
编辑:单词单位并不重要。让我们说我认为单位更大一些。这不是问题所在。真正的问题是为什么对所有方法进行测试或更多测试,因为很少有涵盖许多方法的测试更简单。
示例:列表类。我为什么要单独进行添加和删除测试?首先添加的一个测试会删除声音更简单。
答案 0 :(得分:75)
只测试一件事会隔离一件事并证明它是否有效。这是单元测试的想法。测试不止一件事的测试没有错,但这通常被称为集成测试。根据具体情况,它们都有优点。
举个例子,如果您的床头灯没有打开,并且您更换灯泡并切换延长线,您不知道哪个更改解决了问题。应该进行单元测试,并将您的顾虑分开以隔离问题。
答案 1 :(得分:64)
我将在这里走出困境,并说“唯一测试一件事”的建议实际上没有实际帮助,因为它有时会成为现实。
有时候测试需要一定的设置。有时他们甚至可能需要一定量的时间进行设置(在现实世界中)。通常你可以一次测试两个动作。
Pro:只进行一次所有设置。第一次行动后的测试将证明世界是您在第二次行动之前的预期。更少的代码,更快的测试运行。
Con:如果 操作失败,您将获得相同的结果:相同的测试将失败。关于问题所在的信息比在两次测试中只有一次操作的情况要少。
实际上,我发现这里的“骗局”并不是什么大问题。堆栈跟踪通常会很快缩小范围,我将确保无论如何都要修复代码。
这里稍微不同的“con”是它打破了“编写新测试,使其通过,重构”循环。我认为这是一个理想的循环,但并不总是反映现实。有时,在当前测试中添加额外操作并检查(或者可能只是对现有操作进行另一次检查)比创建新操作更为实用。
答案 2 :(得分:12)
通常不建议检查不止一件事,因为它们更紧密耦合且易碎。如果您在代码中更改某些内容,则更改测试需要更长时间,因为需要考虑更多内容。
[编辑:] 好吧,说这是一个样本测试方法:
[TestMethod]
public void TestSomething() {
// Test condition A
// Test condition B
// Test condition C
// Test condition D
}
如果您对条件A的测试失败,那么B,C和D似乎也会失败,并且不会为您提供任何有用的信息。如果你的代码更改会导致C失败怎么办?如果你把它们分成4个单独的测试,你会知道这一点。
答案 3 :(得分:11)
Haaa ......单元测试。
将任何“指令”推得太远,它会很快变得无法使用。
单个单元测试测试单个方法就像单个方法执行单个任务一样好。但IMHO并不意味着单个测试只能包含一个断言声明。
时
@Test
public void checkNullInputFirstArgument(){...}
@Test
public void checkNullInputSecondArgument(){...}
@Test
public void checkOverInputFirstArgument(){...}
...
优于
@Test
public void testLimitConditions(){...}
是我认为的品味问题,而不是良好的做法。我个人更喜欢后者。
但是
@Test
public void doesWork(){...}
实际上是“指令”要求你不惜一切代价避免的,以及最快的耗尽我的理智。
作为最终结论,将语义相关且易于测试的内容组合在一起,以便失败的测试消息本身实际上足够有意义,可以直接转到代码。
对失败的测试报告的经验法则:如果您必须首先阅读测试代码,那么您的测试结构不够好,需要更多分成更小的测试。
我的2美分。
答案 4 :(得分:7)
想想建一辆车。如果你要应用你的理论,只测试大事,那么为什么不进行测试以驾驶汽车穿越沙漠。它崩溃了。好的,请告诉我导致问题的原因。你不能。这是一个场景测试。
功能测试可能是打开引擎。它失败。但这可能是由于多种原因造成的。你仍然无法确切地告诉我是什么导致了这个问题。我们越来越近了。
单元测试更具体,首先会识别代码被破坏的位置,但它也会(如果正确执行TDD)帮助将代码构建为清晰的模块化块。
有人提到过使用堆栈跟踪。算了吧。那是第二个度假胜地。通过堆栈跟踪或使用调试是一件痛苦的事情并且可能非常耗时。特别是在较大的系统和复杂的错误上。
单元测试的良好特征:
答案 5 :(得分:6)
使用测试驱动开发,您将首先编写测试,然后编写代码以通过测试。如果你的测试是专注的,这使得编写代码更容易通过测试。
例如,我可能有一个带参数的方法。我首先想到的一件事是,如果参数为null,会发生什么?它应该抛出一个ArgumentNull异常(我认为)。所以我编写了一个测试,检查在传递null参数时是否抛出该异常。运行测试。好的,它会抛出NotImplementedException。我通过更改代码来修复它以抛出ArgumentNull异常。运行我的测试它通过。然后我想,如果它太小或太大会发生什么?啊,那是两个测试。我先写一个太小的案例。
关键是我没有同时考虑该方法的行为。我通过考虑它应该做什么来逐步(和逻辑地)构建它,然后实现代码和重构,因为我去使它看起来漂亮(优雅)。这就是为什么测试应该小而且集中,因为当你考虑行为时,你应该以小的,可理解的增量进行开发。
答案 6 :(得分:4)
如果您正在测试多个东西,那么它被称为集成测试...而不是单元测试。您仍然可以在与单元测试相同的测试框架中运行这些集成测试。
集成测试通常较慢,单元测试很快,因为所有依赖项都是模拟/伪造的,因此没有数据库/ Web服务/慢速服务调用。
我们在提交源代码控制时运行单元测试,而我们的集成测试只在每晚构建中运行。
答案 7 :(得分:4)
测试只验证一件事,使故障排除更容易。这并不是说你不应该有测试多个东西的测试,或者多个共享相同设置/拆卸的测试。
这应该是一个说明性的例子。假设你有一个带有查询的堆栈类:
和改变堆栈的方法
现在,请考虑以下测试用例(我在本例中使用Python代码伪代码。)
class TestCase():
def setup():
self.stack = new Stack()
def test():
stack.push(1)
stack.push(2)
stack.pop()
assert stack.top() == 1, "top() isn't showing correct object"
assert stack.getSize() == 1, "getSize() call failed"
从这个测试用例中,您可以确定某些内容是否有问题,但不能确定它是否与push()
或pop()
实现隔离,或者是否返回值top()
和getSize()
。
如果我们为每个方法及其行为添加单独的测试用例,事情变得更容易诊断。此外,通过为每个测试用例进行全新设置,我们可以保证问题完全在失败的测试方法调用的方法中。
def test_size():
assert stack.getSize() == 0
assert stack.isEmpty()
def test_push():
self.stack.push(1)
assert stack.top() == 1, "top returns wrong object after push"
assert stack.getSize() == 1, "getSize wrong after push"
def test_pop():
stack.push(1)
stack.pop()
assert stack.getSize() == 0, "getSize wrong after push"
就测试驱动的开发而言。我个人写了更大的“功能测试”,最初测试了多个方法,然后在我开始实现单个部分时创建单元测试。
另一种看待它的方法是单元测试验证每个单独方法的合同,而较大的测试验证对象和整个系统必须遵循的合同。
我仍在test_push
中使用三个方法调用,但top()
和getSize()
都是通过单独测试方法测试的查询。
您可以通过向单个测试添加更多断言来获得类似的功能,但之后的断言失败将被隐藏。
答案 8 :(得分:3)
GLib,但希望仍然有用,答案是单位= 1。如果你测试不止一件事,那么你就不是单元测试了。
答案 9 :(得分:3)
较小的单元测试可以更清楚地解决问题何时失败。
答案 10 :(得分:2)
关于您的示例:如果您在同一单元测试中测试添加和删除,您如何验证该项目是否已添加到列表中?这就是为什么你需要添加并验证它是在一次测试中添加的原因。
或者使用灯泡示例:如果您想测试灯泡,而您所做的只是打开然后关闭开关,您怎么知道灯泡是否开启?您必须采取介于两者之间的步骤来查看指示灯并验证它是否已打开。然后你可以关闭它并确认它已关闭。
答案 11 :(得分:2)
我支持单元测试只能测试一件事的想法。我也偏离它了很多。今天我进行了测试,其中昂贵的设置似乎迫使我在每次测试中做出多个断言。
namespace Tests.Integration
{
[TestFixture]
public class FeeMessageTest
{
[Test]
public void ShouldHaveCorrectValues
{
var fees = CallSlowRunningFeeService();
Assert.AreEqual(6.50m, fees.ConvenienceFee);
Assert.AreEqual(2.95m, fees.CreditCardFee);
Assert.AreEqual(59.95m, fees.ChangeFee);
}
}
}
与此同时,我真的希望看到所有失败的断言,而不仅仅是第一个断言。我期待他们都失败了,我需要知道我真正回归的数量。但是,每个测试分开的标准[SetUp]将导致3次调用慢速服务。突然间,我想起了一篇文章,建议使用“非常规”测试结构是隐藏单元测试的一半好处。 (我认为这是Jeremy Miller的帖子,但现在找不到它。)突然想到[TestFixtureSetUp],我意识到我可以打一个服务电话,但仍然有单独的,富有表现力的测试方法。
namespace Tests.Integration
{
[TestFixture]
public class FeeMessageTest
{
Fees fees;
[TestFixtureSetUp]
public void FetchFeesMessageFromService()
{
fees = CallSlowRunningFeeService();
}
[Test]
public void ShouldHaveCorrectConvenienceFee()
{
Assert.AreEqual(6.50m, fees.ConvenienceFee);
}
[Test]
public void ShouldHaveCorrectCreditCardFee()
{
Assert.AreEqual(2.95m, fees.CreditCardFee);
}
[Test]
public void ShouldHaveCorrectChangeFee()
{
Assert.AreEqual(59.95m, fees.ChangeFee);
}
}
}
此测试中有更多代码,但它通过向我显示所有与预期不符合的值来提供更多价值。
一位同事还指出,这有点像Scott Bellware的specunit.net:http://code.google.com/p/specunit-net/
答案 12 :(得分:2)
如果您测试多件事并且您测试的第一件事失败,您将无法知道您正在测试的后续内容是通过还是失败。当你知道所有失败的事情时,它会更容易修复。
答案 13 :(得分:1)
非常精细的单元测试的另一个实际缺点是它打破了DRY principle。我曾参与过规则的项目,即每个类的公共方法必须进行单元测试([TestMethod])。显然,每次创建公共方法时都会增加一些开销,但真正的问题是它为重构添加了一些“摩擦”。
它类似于方法级别的文档,它很高兴但是它是另一个必须维护的东西,它使得更改方法签名或名称更麻烦并减慢“牙线重构”(如{{3中所述)由Emerson Murphy-Hill和Andrew P. Black撰写.PDF,1.3 MB)。
与设计中的大多数事情一样,需要权衡“测试应该只测试一件事”这一短语并不会被捕获。
答案 14 :(得分:1)
当测试失败时,有三个选项:
使用描述性名称进行细粒度测试有助于读者了解为什么编写测试,这反过来又更容易知道选择上述哪个选项。测试的名称应描述测试指定的行为 - 每个测试只有一个行为 - 这样只需读取测试的名称,读者就会知道系统的作用。有关详细信息,请参阅this article。
另一方面,如果一个测试做了很多不同的事情并且它有一个非描述性的名称(例如以实现中的方法命名的测试),那么很难找到其背后的动机。测试时,很难知道何时以及如何更改测试。
当每个测试只测试一件事时,这是它的外观(使用GoSpec):
func StackSpec(c gospec.Context) {
stack := NewStack()
c.Specify("An empty stack", func() {
c.Specify("is empty", func() {
c.Then(stack).Should.Be(stack.Empty())
})
c.Specify("After a push, the stack is no longer empty", func() {
stack.Push("foo")
c.Then(stack).ShouldNot.Be(stack.Empty())
})
})
c.Specify("When objects have been pushed onto a stack", func() {
stack.Push("one")
stack.Push("two")
c.Specify("the object pushed last is popped first", func() {
x := stack.Pop()
c.Then(x).Should.Equal("two")
})
c.Specify("the object pushed first is popped last", func() {
stack.Pop()
x := stack.Pop()
c.Then(x).Should.Equal("one")
})
c.Specify("After popping all objects, the stack is empty", func() {
stack.Pop()
stack.Pop()
c.Then(stack).Should.Be(stack.Empty())
})
})
}
答案 15 :(得分:1)
真正的问题是为什么对所有方法进行测试或更多测试,因为很少有涵盖许多方法的测试更简单。
那么,当某些测试失败时,您就会知道哪种方法失败了。
当您必须修理无功能的汽车时,如果您知道发动机的哪个部分出现故障,则会更容易。
示例:列表类。我为什么要单独进行添加和删除测试?首先添加的一个测试会删除声音更简单。
假设添加方法已被破坏且未添加,并且删除方法已被破坏且未删除。您的测试将检查添加和删除后的列表是否与最初的大小相同。你的测试会成功。虽然你的两种方法都会被打破。
答案 16 :(得分:0)
免责声明:这是一本受到本书" xUnit测试模式"高度影响的答案。
每次测试只测试一件事是提供以下好处的最基本原则之一:
我只能看到为什么你可以从测试验证多个事情中受益的一个原因,但实际上这应该被视为代码味道: