您的单元测试有多深?


88

我发现的有关TDD的事情是,它需要花费一些时间来设置您的测试,并且自然而然地变得懒惰,我一直想编写尽可能少的代码。我似乎要做的第一件事是测试我的构造函数是否设置了所有属性,但是这太过分了吗?

我的问题是编写单元测试的粒度级别是多少?

..是否有太多测试案例?

Answers:


221

我从有效的代码而不是测试中获得报酬,所以我的理念是尽可能少地测试以达到给定的置信度(我怀疑与行业标准相比,此置信度高,但这可能只是自负) 。如果我通常不会犯某种错误(例如在构造函数中设置错误的变量),那么我不会对其进行测试。我确实倾向于理解测试错误,所以当我有复杂条件的逻辑时,我要格外小心。在团队中进行编码时,我会修改策略以仔细测试我们共同容易出错的代码。

基于这种哲学,不同的人将有不同的测试策略,但是鉴于我对测试如何最好地适合于编码内循环的理解还不成熟,这对我来说似乎是合理的。从现在起的十到二十年,我们可能会有一个更为通用的理论,即要编写哪些测试,不编写测试以及如何区分差异。同时,实验似乎是有序的。


40
全世界都不认为肯特·贝克会这样说!有许多开发人员尽职尽责地追求100%的覆盖率,因为他们认为这是Kent Beck会做的!我已经告诉过许多人,您在XP书籍中说过,您并不总是虔诚地遵守Test First。但是我也很惊讶。
Charlie Flowers

6
实际上不同意,因为开发人员生成的代码不是他自己的,下一次冲刺将由其他人更改它并提交您“不知道”的错误。另外,TDD您首先考虑测试。因此,如果您假设测试部分代码而进行TDD,则说明您做错了
Ricardo Rodrigues 2012年

2
我对覆盖范围不感兴趣。我对Beck先生多久提交一次未针对测试失败而编写的代码感到非常感兴趣。
sheldonh 2012年

1
@RicardoRodrigues,您不能编写测试来掩盖其他人稍后编写的代码。那是他们的责任。
基夫

2
那不是我写的,请仔细阅读;我写道,如果您编写测试以仅覆盖您自己的代码的一部分,而将未发现的部分留在“您知道自己没有犯错”的地方,并且这些部分被更改并且没有适当的测试,那么您就遇到了问题,这根本不是TDD。
里卡多·罗德里格斯

20

编写单元测试以了解您可能会遇到的问题以及极端情况。此后,应在引入错误报告时添加测试用例-在编写该错误的修复程序之前。然后,开发人员可以确信:

  1. 该错误已修复;
  2. 该错误不会再次出现。

根据附带的注释-如果随着时间的推移在给定的类中发现大量错误,我猜这种编写单元测试的方法可能会引起问题。这可能是在谨慎处理方面有所帮助的地方-仅对可能再次发生的错误添加单元测试,或者重新发生可能导致严重问题的错误。我发现,在这些情况下,对单元测试中的集成测试进行度量可能会有所帮助-在较高的代码路径中测试代码可以覆盖较低的代码路径。


随着我编写的大量错误,这可能会成为一种反模式。如果对代码进行了数百次测试,那么事情就坏了,这可能意味着您的测试变得不可读,并且当需要重写这些测试时,这可能会成为开销。
约翰诺·诺兰

@JohnNolan:测试的可读性重要吗?恕我直言,至少对于这些特定于错误的回归测试而言并非如此。如果您经常重写测试,则可能是在较低级别进行测试-理想情况下,即使您的实现发生更改,您的接口也应保持相对稳定,并且应该在接口级别进行测试(尽管我知道现实世界经常是“像这样...:-/)如果您的界面发生了很大的变化,我宁愿废弃大多数或所有这些特定于bug的测试,而不是重写它们。
j_random_hacker 2012年

@j_random_hacker是的,可读性当然很重要。测试是一种文档形式,与生产代码同样重要。我确实同意取消重大更改的测试是一件好事(tm),并且应该在接口级别进行测试。
Johnno Nolan

19

一切都应该尽可能简单,但不要简单。-爱因斯坦

关于TDD的最令人误解的事情之一就是其中的第一个单词。测试。这就是BDD出现的原因。因为人们并不真正了解第一个D是重要的一个,即Driven。我们都倾向于对测试多加考虑,而对设计驱动力则少加考虑。而且我想这是对您的问题的一个模糊的答案,但是您可能应该考虑如何驱动代码,而不是实际测试的代码。Coverage工具可以为您提供帮助。设计是一个更大,更成问题的问题。


是的,它含糊不清...这是否意味着作为构造函数不属于部分行为,我们不应该对其进行测试。但是我应该测试MyClass.DoSomething()吗?
Johnno Nolan

好吧,取决于:P ...在尝试测试遗留代码时,构造测试通常是一个好的开始。但是,当从头开始设计某些东西时,我可能会(在大多数情况下)不进行构造测试。
kitofr

它是驱动开发,而不是驱动设计。意思是,获得一个可行的基准,编写测试以验证功能,并继续进行开发。我几乎总是在第一次考虑一些代码之前就编写测试。
Evan Plaice 2010年

我要说的最后一个D(设计)是人们忘记的词,从而失去了焦点。在测试驱动的设计中,您针对失败的测试编写代码。如果您正在执行测试驱动的设计,那么最终会得到多少未经测试的代码?
sheldonh 2012年

15

对于那些建议进行“一切”测试的人:意识到“全面测试”这样的方法int square(int x)需要使用通用语言和典型环境的大约40亿个测试用例。

事实上,它甚至更糟的是:一种方法void setX(int newX)也有责任改变之外的任何其他成员的价值观x-你测试的是obj.yobj.z等所有调用后保持不变obj.setX(42);

测试“一切”的子集是唯一可行的。 一旦您接受了这一点,考虑不测试难以置信的基本行为就变得更加可口了。每个程序员都有错误位置的概率分布。明智的方法是将精力集中在您估计错误概率很高的测试区域上。


9

经典的答案是“测试任何可能会损坏的东西”。我将其解释为意味着测试setter和getter除了set或get以外不执行任何其他操作的测试可能是过多的测试,无需花费时间。除非您的IDE为您编写了这些代码,否则您也可能会这样做。

如果您的构造函数设置属性,以后可能会导致错误,那么测试设置这些属性并不过分。


是的,这是对具有许多属性和许多构造函数的类的绑定。
约翰诺·诺兰

问题越琐碎(例如忘记将成员初始化为零),则调试时间将越长。
列夫

5

我编写测试以涵盖将要编写的类的假设。测试执行要求。本质上,例如,如果x永远不能为3,则我将确保有一项测试可以满足该要求。

总是,如果我不编写覆盖条件的测试,它将在以后的“人工”测试中出现。我当然会写一个,但是我宁愿早点抓到它们。我认为重点是测试(可能)很乏味但很有必要。我编写了足够的测试来完成测试,但仅此而已。


5

现在跳过简单测试的部分问题在于将来,重构可能会使这个简单的属性变得非常复杂,并带有很多逻辑。我认为最好的主意是您可以使用“测试”来验证模块的要求。如果您通过X时应该将Y退回,那么这就是您要测试的内容。然后,当您稍后更改代码时,可以验证X是否为Y,并且以后添加此要求时,可以为A添加测试以为B。

我发现我在最初的开发中花费的时间用于编写测试会在第一个或第二个错误修复中获得回报。能够拾起3个月内未曾查看过的代码,并能合理地确定您的修复程序能涵盖所有情况,并且“可能”不会破坏任何内容的能力非常有价值。您还将发现,单元测试将帮助您对错误进行分类,而不仅仅是堆栈跟踪等等。查看应用程序的各个部分如何工作和失败,可以使您深入了解它们为什么整体工作或失败。


4

在大多数情况下,我会说,如果那里有逻辑,请对其进行测试。这包括构造函数和属性,尤其是在属性中设置了多个内容时。

关于过多的测试,这值得商bat。有些人会说应该对所有内容进行健壮性测试,而其他人则说要进行有效的测试,仅应测试可能破坏的事物(即逻辑)。

我会更倾向于第二营,仅凭个人经验,但如果有人确实决定测试所有东西,我不会说这太多了……也许对我来说有点过大,但对他们来说却不过分。

所以,不-我要说的是,一般意义上的测试“不太多”,仅针对个人。


3

测试驱动开发意味着您通过所有测试后就停止编码。

如果您没有针对某个属性的测试,那么为什么要实现它呢?如果您没有测试/定义“非法”分配的预期行为,那么该属性应该怎么做?

因此,我完全愿意测试一个班级应该表现出的每一种行为。包括“原始”属性。

为了简化测试,我创建了一个简单的NUnit TestFixture,它提供了用于设置/获取值的扩展点,并获取有效值和无效值的列表,并进行了一次测试以检查该属性是否正常工作。测试单个属性如下所示:

[TestFixture]
public class Test_MyObject_SomeProperty : PropertyTest<int>
{

    private MyObject obj = null;

    public override void SetUp() { obj = new MyObject(); }
    public override void TearDown() { obj = null; }

    public override int Get() { return obj.SomeProperty; }
    public override Set(int value) { obj.SomeProperty = value; }

    public override IEnumerable<int> SomeValidValues() { return new List() { 1,3,5,7 }; }
    public override IEnumerable<int> SomeInvalidValues() { return new List() { 2,4,6 }; }

}

使用lambda和属性,甚至可以更紧凑地编写。我搜集到的MBUnit甚至对这种东西有一些本机支持。不过,关键是上面的代码捕获了属性的意图。

PS:也许PropertyTest还应该具有一种检查对象上其他属性是否不变的方法。嗯..回到绘图板上。


我参加了关于mbUnit的演示。看起来不错。
Johnno Nolan

但是戴维,让我问你:你对肯特·贝克的上述回应感到惊讶吗?他的回答让您怀疑是否应该重新考虑自己的方法?当然,不是因为任何人都有“很高的答案”。但是,肯特首先被认为是测试的核心支持者之一。一分钱的想法!
Charlie Flowers

@查理:肯特的回应非常务实。我“只是”工作的一个项目,我会从各种渠道整合代码,我想提供一个非常高度信心。
David Schmitt,2009年

就是说,我的确致力于使测试比经过测试的代码更简单,并且只有在所有生成器,模块,业务规则和验证器都在一起的集成测试中,这种级别的详细信息才值得。
David Schmitt,2009年

1

我进行单元测试以达到最大可行范围。如果我无法找到某些代码,则将重构直到覆盖范围尽可能完整为止

在完成了盲目的编写测试之后,我通常会编写一个测试用例来重现每个错误

我习惯于在代码测试和集成测试之间进行区分。在集成测试期间(这也是单元测试,但在组件组上,因此不完全是单元测试的目的),我将测试正确实现的需求。


1

因此,我通过编写测试来驱动程序的次数越多,对测试粒度的担心就越少。回顾过去,看来我正在做最简单的事情来实现验证行为的目标。这意味着我对我的代码正在执行我要求做的事情产生信心,但是,这不能绝对保证我的代码没有错误。我认为正确的平衡是测试标准行为,也许是一两个极端的案例,然后继续进行设计的下一部分。

我接受这不会涵盖所有错误,并使用其他传统的测试方法来捕获这些错误。


0

通常,我从小做起,以我知道必须起作用的输入和输出。然后,在修复错误时,我添加了更多测试,以确保对已修复的内容进行测试。它是有机的,对我来说效果很好。

你可以测试太多吗?可能吧,但是总体上谨慎一点可能会更好,尽管它取决于应用程序的关键任务。


0

我认为您必须测试业务逻辑“核心”中的所有内容。Getter和Setter也是如此,因为它们可以接受您可能不想接受的负值或空值。如果您有时间(总是取决于您的老板),最好测试其他业务逻辑和调用这些对象的所有控制器(您从单元测试缓慢地转到集成测试)。


0

我不会对没有副作用的简单的setter / getter方法进行单元测试。但是我对其他所有公共方法都进行单元测试。我尝试为算法中的所有边界条件创建测试,并检查单元测试的覆盖范围。

它的工作量很大,但我认为值得。我宁愿编写代码(甚至是测试代码),也不愿在调试器中单步执行代码。我发现代码-构建-部署-调试周期非常耗时,并且集成到构建中的单元测试越详尽,花费在该代码-构建-部署-调试周期中的时间就越少。

您没有说过为什么还要编码。但是对于Java,我使用Maven 2JUnitDbUnitCoberturaEasyMock


我没有说哪个是与语言无关的问题。
Johnno Nolan

TDD中的单元测试不仅可以覆盖编写代码的过程,还可以防止继承代码的人,然后认为在getter中格式化值是有道理的!
Paxic

0

我读得越多,我就越认为某些单元测试就像一些模式:语言不足的味道。

当您需要测试琐碎的getter是否实际返回正确的值时,这是因为您可能会混用getter名称和成员变量名称。输入ruby的'attr_reader:name',这将不再发生。只是在Java中是不可能的。

如果您的吸气剂变得不平凡,您仍然可以为其添加测试。


我同意测试吸气剂是微不足道的。但是我可能很愚蠢,以至于忘记在构造函数中进行设置。因此,需要进行测试。自问了问题以来,我的想法已经改变。见我的回答stackoverflow.com/questions/153234/how-deep-are-your-unit-tests/...
Johnno诺兰

1
实际上,我认为在某种程度上,单元测试作为一个整体是一种语言问题的味道。支持合同(方法的前提条件)的语言(如Eiffel)仍需要一些单元测试,但确实需要较少的单元测试。在实践中,即使是简单的合同也非常容易找到错误:当某个方法的合同中断时,该方法中通常存在错误。
Damien Pollet

@Damien:也许单元测试和合同实际上是变相的东西?我的意思是,一种“支持”合同的语言基本上可以轻松编写代码段(测试),这些代码段(可选)在其他代码段之前和之后执行,对吗?如果其语法足够简单,那么可以通过编写预处理器来轻松地扩展不支持合同的语言,以支持合同,对吗?还是有些方法(合同或单元测试)可以做到而其他方法却做不到?
j_random_hacker

0

测试使您担心的源代码。

只要您不会出错,对测试您很有信心的部分代码就没有用。

测试错误修正,以便这是您第一次和最后一次修正错误。

测试以获取晦涩的代码部分的可信度,以便您创建知识。

在进行大量重构和中等重构之前进行测试,以免破坏现有功能。


0

这个答案更多的是要弄清楚由于给定的方法的重要性/重要性,您知道要对给定方法使用多少个单元测试。使用McCabe的基础路径测试技术,您可以执行以下操作来定量地获得比简单的“语句覆盖率”或“分支覆盖率”更好的代码覆盖率置信度:

  1. 确定要进行单元测试的方法的Cyclomatic Complexity值(例如,Visual Studio 2010 Ultimate可以使用静态分析工具为您计算该值;否则,您可以通过流程图方法-http://users.csc手动进行计算。 calpoly.edu/~jdalbey/206/Lectures/BasisPathTutorial/index.html
  2. 列出通过方法流动的独立路径的基础集-有关流程图示例,请参见上面的链接
  3. 为步骤2中确定的每个独立基础路径准备单元测试

您将针对每种方法执行此操作吗?认真吗
克里斯托弗·约翰逊
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.