为什么unit testing只testing一件事?
什么是一个好的unit testing? 说一个testing应该只testing一件事情。 这有什么好处?
写一个更大的testing来testing更大的代码块不是更好吗? 调查一个testing失败是困难的,我没有看到从较小的testing帮助。
编辑:单位这个词并不重要。 比方说,我认为单位有点大。 这不是这个问题。 真正的问题是,为什么对所有方法进行testing或更多testing,因为很less涉及许多方法的testing更简单。
一个例子:一个列表类。 为什么我应该单独testing添加和删除? 首先添加一个testing,然后删除听起来更简单。
我会在这里出去走一走,说“只testing一件事”的build议并不像实际上有用的那样有帮助。
有时testing需要一定的设置。 有时他们甚至可能需要一定的时间来build立(在现实世界中)。 通常你可以一口气testing两个动作。
专业版:只有所有的设置发生一次。 在第一个动作之后,你的testing将certificate这个世界是你在第二个动作之前所期望的。 更less的代码,更快的testing运行。
答:如果任一操作失败,您将得到相同的结果:相同的testing将失败。 如果在两个testing中只有一个单独的操作,那么问题出现的位置就会比较less。
实际上,我发现这里的“con”并不是什么大问题。 堆栈跟踪通常会缩短事情的速度,而且我会确保无论如何都修复代码。
这里略有不同的“con”是它打破了“写一个新的testing,让它通过,重构”循环。 我认为这是一个理想的循环,但并不总是反映现实。 有时,在当前testing中添加一个额外的操作并检查(或者可能只是对现有操作的另一个检查),而不是创build一个新的操作。
testing只有一件事将隔离这一件事,并certificate它是否有效。 这是unit testing的想法。 testing不止一件事情的testing没有错,但通常被称为集成testing。 基于上下文,它们都有优点。
举个例子,如果你的床头灯没有打开,而你更换灯泡并切换延长线,你不知道哪个改变解决了问题。 应该已经完成了unit testing,并将问题分离出来以隔离问题。
通常不build议检查多个事物的testing,因为它们更加紧密和脆弱。 如果您在代码中更改了某些内容,则需要更长的时间来更改testing,因为还有更多的事情需要考虑。
好的,说这是一个示例testing方法:
[TestMethod] public void TestSomething() { // Test condition A // Test condition B // Test condition C // Test condition D }
如果您对条件A的testing失败,那么B,C和D也会失败,并且不会为您提供任何用处。 如果你的代码改变会导致C失败呢? 如果你把它们分成4个单独的testing,你会知道这一点。
Haaa …unit testing。
推动任何“指令”太多,它迅速变得无法使用。
单一unit testingtesting单一事物就像单一方法完成单一任务一样好。 但恕我直言,这并不意味着一个单一的testing只能包含一个断言语句。
是
@Test public void checkNullInputFirstArgument(){...} @Test public void checkNullInputSecondArgument(){...} @Test public void checkOverInputFirstArgument(){...} ...
比…更好
@Test public void testLimitConditions(){...}
在我看来是品味的问题,而不是好的做法。 我个人更喜欢后者。
但
@Test public void doesWork(){...}
实际上是“指令”希望你不惜一切代价避免的,什么使我的理智最快地stream失了。
作为最后的结论,把语义上相关的东西和易于testing的东西组合在一起,这样一个失败的testing消息本身对于你直接进入代码是足够有意义的。
在失败的testing报告中,经验法则是:如果您必须先阅读testing代码,那么您的testing结构不够好,需要更多的testing。
我的2美分。
想想build一辆车。 如果你要运用你的理论,只是testing大的东西,那么为什么不做一个testing,驾驶汽车穿越沙漠。 它打破了。 好的,告诉我是什么原因造成了这个问题。 你不能。 这是一个场景testing。
functiontesting可能是打开引擎。 它失败。 但这可能是由于一些原因。 你仍然无法确切地告诉我是什么原因造成了这个问题。 尽pipe我们正在接近。
unit testing更具体,首先将识别代码被破坏的位置,但也会(如果执行适当的TDD)帮助您将代码构build成清晰的模块化块。
有人提到使用堆栈跟踪。 算了吧。 这是第二个度假胜地。 通过堆栈跟踪,或使用debugging是一个痛苦,可能是耗时的。 特别是在较大的系统和复杂的错误。
unit testing的好特性:
- 快(毫秒)
- 独立。 它不受其他testing的影响或依赖于其他testing
- 明确。 它不应该臃肿,或包含大量的设置。
使用testing驱动的开发,你会先写你的testing,然后编写代码来通过testing。 如果你的testing重点,这使得编写代码更容易通过testing。
例如,我可能有一个方法需要一个参数。 我首先想到的一件事是,如果参数为空,会发生什么? 它应该抛出一个ArgumentNullexception(我认为)。 所以我写了一个testing,检查是否在传递null参数时引发exception。 运行testing。 好吧,它会抛出NotImplementedException。 我通过更改代码来解决抛出一个ArgumentNullexception。 运行我的testing通过。 那么我想,如果它太小或太大会发生什么? 啊,这是两个testing。 我先写一个太小的案例。
关键是我不会一下子想到这个方法的行为。 我通过思考它应该做什么来递增(并且逻辑)地构build它,然后实现代码和重构,让它看起来很漂亮(优雅)。 这就是为什么testing应该小而专注的原因,因为当你考虑行为时,应该以小的,可以理解的增量来开发。
进行testing,只validation一件事情,使解决问题更容易。 这并不是说你不应该有testing多个东西,或多个testing共享相同的设置/拆解。
这里应该是一个说明性的例子。 假设你有一个带查询的堆栈类:
- 的getSize
- 是空的
- 共达
和方法来改变堆栈
- 推(anObject)
- stream行()
现在,考虑下面的testing用例(对于这个例子,我使用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"
从这个testing用例中,您可以确定是否有问题,而不是它是否与push()
或pop()
实现隔离,或者返回值为top()
和getSize()
。
如果我们为每个方法及其行为添加单独的testing用例,事情变得更容易诊断。 另外,通过对每个testing用例进行全新的设置,我们可以保证问题完全在失败的testing方法调用的方法中。
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"
就testing驱动开发而言。 我个人编写了大型的“functiontesting”,最终testing多个方法,然后创buildunit testing,因为我开始实施个别部分。
另一种看待它的方法是unit testingvalidation每个单独的方法的合同,而较大的testingvalidation整个对象和系统必须遵循的合同。
我仍然在test_push
使用三个方法调用,但是top()
和getSize()
都是由单独的testing方法testing的查询。
您可以通过向单个testing添加更多断言来获得类似的function,但是之后的断言失败将被隐藏。
如果你正在testing多个东西,那么它被称为集成testing…而不是unit testing。 您仍然可以在unit testing的相同testing框架中运行这些集成testing。
集成testing通常比较慢,unit testing很快,因为所有的依赖关系都被嘲弄/伪造,所以没有数据库/networking服务/缓慢的服务调用。
我们在提交源代码控制时运行unit testing,而我们的集成testing只能在夜间构build中运行。
较小的unit testing可以更清楚地说明何时发生问题。
GLib,但希望仍然有用,答案是单位= 1。 如果你testing多个东西,那么你不是unit testing。
如果你testing了多件事情,而且你testing的第一件事情失败了,你将不知道你testing的后续事情是否通过了。 当你知道所有会失败的事情时,修复起来会更容易。
关于你的例子:如果你正在testing在同一个unit testing中添加和删除,你如何validation该项目是否添加到你的列表? 这就是为什么您需要添加并validation它是在一个testing中添加的。
或者使用灯的例子:如果你想testing你的灯,你所做的只是打开开关,然后closures,你怎么知道灯曾经打开? 您必须在两者之间观察灯并确认它已打开。 然后,您可以将其closures并validation是否已closures。
我支持unit testing只应该testing一件事的想法。 我也偏离了这一点。 今天我做了一个testing,昂贵的设置似乎迫使我做每个testing多个断言。
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); } } }
同时,我真的很想看到我所有的失败的主张,而不仅仅是第一个。 我期待他们都失败了,我需要知道我真的回来了多less。 但是,每个testing划分的标准[SetUp]会导致3次调用慢速服务。 突然间,我想起了一篇文章,build议使用“非常规”的testing结构是隐藏unit testing的一半好处的地方。 (我想这是一个Jeremy Miller的post,但是现在找不到了。)突然想起[TestFixtureSetUp],我意识到我可以做一个单一的服务调用,但是仍然有独立的,富有performance力的testing方法。
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); } } }
在这个testing中有更多的代码,但是通过向我展示所有与预期不符的值,它提供了更多的价值。
一位同事也指出,这有点像Scott Bellware的specunit.net:http://code.google.com/p/specunit-net/
非常详细的unit testing的另一个实际缺点是它打破了DRY原则 。 我曾经在一个规则是,每个类的公共方法必须有一个unit testing([TestMethod])的项目。 显然,这在每次创build公共方法时都会增加一些开销,但真正的问题在于它为重构添加了一些“摩擦”。
它类似于方法级别的文档,很高兴有,但它是另一件事必须维护,它使方法签名或名称更改麻烦一些,并减缓“牙线重构”(如“重构工具:适用于目的“由艾默生墨菲山和安德鲁体育黑色PDF,1.3 MB)。
像devise中的大多数事情一样,“testing应该只testing一件事”这个短语是没有意义的。
testing失败时,有三种select:
- 实施已经中断,应该修复。
- testing已经中断,应该修复。
- testing不再需要,应该删除。
具有描述性名称的细粒度testing有助于读者了解为什么testing被写入,这又使得更容易知道上述哪个选项可供select。 testing的名称应该描述testing指定的行为 – 每个testing只有一个行为 – 这样,只要读取testing的名字,读者就可以知道系统是干什么的。 有关更多信息,请参阅此文章 。
另一方面,如果一个testing做了很多不同的事情,而且它有一个非描述性的名字(例如在实现中以方法命名的testing),那么很难找出testing背后的动机,这将是很难知道什么时候以及如何改变testing。
下面是它可以看起来像什么(与GoSpec ),当每个testing只testing一件事情:
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()) }) }) }
真正的问题是,为什么对所有方法进行testing或更多testing,因为很less涉及许多方法的testing更简单。
那么,这样,当一些testing失败,你知道哪个方法失败。
当你需要修理一个没有function的汽车时,当你知道发动机的哪个部分发生故障的时候会比较容易。
一个例子:一个列表类。 为什么我应该单独testing添加和删除? 首先添加一个testing,然后删除听起来更简单。
假设添加方法被破坏,不会添加,并且删除方法被破坏并且不会被删除。 您的testing将检查添加和删除后的列表与最初的大小相同。 你的testing将会成功。 虽然你的两种方法都会被打破。
免责声明:这是一个高度受“xUnittesting模式”书影响的答案。
在每个testing中只testing一件事是最基本的原则之一,它提供了以下好处:
- 缺陷本地化 :如果一个testing失败,您立即知道它为什么失败(理想情况下,没有进一步的故障排除,如果你使用的断言做得很好)。
- 作为规范进行testing:testing不仅作为安全networking,而且可以轻松用作规范/文档。 例如,开发人员应该能够读取单个组件的unit testing,并理解API的合同,而无需阅读实现(利用封装的好处)。
- TDD的不可行性 :TDD基于具有小尺寸的function块,并完成渐进迭代(写入失败的testing,写入代码,validationtesting成功)。 如果一个testing需要validation多个事物,那么这个过程会被高度打乱。
- 缺乏副作用 :与第一个有些相关,但是当一个testingvalidation了多个事物时,它更可能与其他testing绑定在一起。 所以,这些testing可能需要有一个共享的testing夹具,这意味着一个会受到另一个的影响。 所以,最终你可能会有一个testing失败,但实际上另一个testing是导致失败的testing,例如通过改变灯具数据。
我只能看到一个原因,为什么你可以从validation多个事情的testing中受益,但是这应该被看作是一个代码异味:
- 性能优化 :在某些情况下,您的testing不仅仅在内存中运行,而且还依赖于持久性存储(例如数据库)。 在某些情况下,通过testing来validation多个事物可能有助于减less磁盘访问次数,从而减less执行时间。 但是,unit testing最好只能在内存中执行,所以如果遇到这种情况,你应该重新考虑一下你是否走错了路。 在unit testing中所有的持久依赖应该被replace为模拟对象。 端到端function应该由不同的集成testing套件覆盖。 这样就不需要再考虑执行时间了,因为集成testing通常是由构buildpipe道而不是由开发人员来执行的,所以略高的执行时间几乎不影响软件开发生命周期的效率。