我们应该测试所有方法吗?


61

所以今天我和队友讨论了单元测试。当他问我“嘿,那堂课的考试在哪儿,我只看到一个?”时,整个事情开始了。整个类都是一个管理器(或服务,如果您喜欢这样称呼它),几乎所有方法都只是将东西委托给DAO,所以它类似于:

SomeClass getSomething(parameters) {
    return myDao.findSomethingBySomething(parameters);
}

一种没有逻辑的样板(或者至少我不认为像逻辑这样的简单委托),但是在大多数情况下(层分离等)很有用。我们进行了相当长时间的讨论,是否应该对它进行单元测试(我认为值得一提的是,我对DAO进行了完全单元测试)。他的主要论据是(显然)它不是TDD,并且有人可能希望查看测试以检查此方法的作用(我不知道它怎么可能更明显),或者将来有人可能希望更改该方法。实现并向其中添加新的(或更多类似“ any”的)逻辑(在这种情况下,我猜有人应该只测试该逻辑)。

不过,这让我思考。我们应该争取最高的测试覆盖率吗?还是仅仅是出于艺术的缘故?我根本看不出在进行以下测试之后有任何原因:

  • getter和setter(除非他们实际上有一些逻辑)
  • “样板”代码

显然,对这种方法(带有模拟)的测试将花费我不到一分钟的时间,但我想这仍然是浪费时间,并且每个CI都需要花费一毫秒以上的时间。

是否有任何合理/不“易燃”的理由来说明为什么要测试每一行(或尽可能多的行)代码?


2
我仍然对这个问题下定决心,但是这是一个已经确定答案为“否”的人的谈话。伊恩·库珀(Ian Cooper):TDD,这一切都出了什么问题要总结这个精彩的演讲,您应该测试由内而外并测试新行为,而不是新方法。
丹尼尔·卡普兰

这真的是一个很棒的演讲,必须看到,让很多人大开眼界,我喜欢它。但是我认为答案不是“否”。它的“是,但间接”。伊恩·库珀(Ian cooper)讨论了六边形架构以及模拟/插拔端口的测试功能/行为。在这种情况下,此端口是DAO的端口,并且此“经理/服务”对其进行的测试不是仅针对该类的单个单元测试,而是通过测试某些功能的“单元测试”(我完全同意的Ian Cooper定义中的单元)进行的您的域中使用此管理器/服务的站点。
AlfredoCasado 2014年


在某种程度上,这将取决于您的系统,如果您正在开发具有中度到高级安全认证水平的系统,则无论琐碎性
jk,

Answers:


48

我遵循肯特·贝克的经验法则:

测试所有可能破坏的东西。

当然,这在某种程度上是主观的。对我来说,像您上面这样的琐碎吸气器/装卸器和单缸吸油机通常不值得。但是话又说回来,我大部分时间都在为遗留代码编写单元测试,只是梦见一个不错的未开发TDD项目...在这些项目上,规则是不同的。对于遗留代码,主要目标是以尽可能少的努力覆盖尽可能多的领域,因此,单元测试往往是更高层次且更复杂的,更像是集成测试(如果人们对术语学得很熟)。而且,当您要使总体代码覆盖率从0%上升或仅设法将其提高到25%以上时,单元测试getter和setter就是您最少的担心。

OTOH在未开发的TDD项目中,即使对于这种方法,编写测试也可能是事关事实。特别是在您已经开始编写测试之前,您有机会开始怀疑“这一行值得进行专门的测试吗?”。而且至少这些测试编写起来很简单并且可以快速运行,所以这两种方法都没什么大不了的。


啊,我完全忘了那句话!猜猜我将它用作我的主要论据,因为坦率地说-在这里有什么可以打破的?不太多。唯一可以中断的是方法调用,如果发生,则意味着发生了非常糟糕的事情。谢谢!
Zenzen 2012年

5
@Zenzen:“这里有什么可以打破的?不是很多。” -所以它破裂。只是一个小错字。或有人添加一些代码。或搞砸了依赖性。我真的认为贝克会声称您的主要榜样符合易碎性。getter和setter,尽管如此,尽管我已经陷入了复制/粘贴错误,但情况并非如此。真正的问题是,如果编写测试太琐碎,为什么它甚至存在?
pdr 2012年

1
您已经花了思考的时间,就可以编写测试了。我说写测试,不要在不写测试的时候留下灰色区域,否则会出现更多破损的窗口。
kett_chup 2012年

1
我要补充一点,我的一般经验是,测试吸气剂和塞脂剂在长期但低优先级方面有些有价值。之所以这样,是因为它现在有“零”机会发现错误的原因,所以您不能保证另一个开发人员不会在三个月内添加任何东西(“只是一个简单的if语句”),而这有可能会被打破。 。进行单元测试可以防止这种情况。同时,这并不是真正的优先事项,因为您不会很快找到任何东西。
dclements

7
盲目测试所有可能破坏的东西都是没有意义的。需要制定一种策略,首先对高风险组件进行测试。
CodeART 2014年

12

单元测试的类型很少:

  • 基于状态。您采取行动,然后断言该对象的状态。例如,我存了一笔钱。然后,我检查余额是否增加。
  • 基于返回值。您对返回值采取行动并主张。
  • 基于互动。您验证您的对象称为另一个对象。这似乎就是您在示例中所做的。

如果您要先编写测试,那么这将更有意义-正如您期望的那样,将调用数据访问层。测试最初会失败。然后,您将编写生产代码以使测试通过。

理想情况下,您应该测试逻辑代码,但是交互(调用其他对象的对象)同等重要。在你的情况下,我会

  • 检查是否使用传入的确切参数调用了数据访问层。
  • 检查它仅被调用过一次。
  • 检查我是否正确返回了数据访问层提供给我的内容。否则,我最好返回null。

当前没有逻辑,但情况并非总是如此。

但是,如果您确信此方法将没有逻辑并且可能保持不变,那么我会考虑直接从使用者调用数据访问层。仅当团队的其他成员都在同一页面上时,我才这样做。您不想通过说“嘿,可以忽略域层,直接调用数据访问层”来向团队发送错误消息。

如果对此方法进行了集成测试,我还将专注于测试其他组件。我还没有看到一家拥有可靠集成测试的公司。

说了这么多-我不会盲目地测试所有内容。我将确定热点(具有高复杂性和高断裂风险的组件)。然后,我将专注于这些组件。拥有一个代码库是没有意义的,其中90%的代码库非常简单,并且可以被单元测试覆盖,而剩下的10%则代表了系统的核心逻辑,并且由于它们的复杂性而未被单元测试覆盖。

最后,测试此方法有什么好处?如果这不起作用会有什么含义?他们是灾难性的吗?不要努力获得较高的代码覆盖率。代码覆盖率应该是一系列好的单元测试的副产品。例如,您可以编写一个测试,该测试可以使您在树上行走,并为您提供此方法的100%覆盖率,或者您可以编写三个单元测试,也可以为您提供100%的覆盖率。区别在于,通过编写三个测试可以测试边缘情况,而不是只走一棵树。


为什么要检查您的DAL仅被调用过一次?
Marjan Venema

9

这是考虑软件质量的好方法:

  1. 类型检查正在解决问题的一部分。
  2. 测试将处理其余部分

对于样板功能和琐碎的功能,您可以依靠类型检查来完成工作,而其余的则需要测试用例。


当然,类型检查仅在您在代码中使用特定类型时才有效,或者您正在使用编译语言,否则您要确保频繁运行静态分析检查(例如,作为CI的一部分)。
bdsl

6

我认为圈复杂度是一个参数。如果方法不够复杂(如getter和setter)。无需单元测试。McCabe的Cyclomatic Complexity级别应大于1。另一个单词应至少包含1个block语句。


请记住,一些吸气剂或塞特剂会产生副作用(尽管不鼓励这样做,并且在大多数情况下被认为是不良做法),因此源代码中的更改也可能会对其产生影响。
2012年

3

TDD响亮的YES(有一些例外)

很有争议,但是我认为,对此问题回答“否”的任何人都缺少TDD的基本概念。

对我来说,如果您遵循TDD ,答案是肯定的。如果不是,那么不合理的答案。

TDD中的DDD

TDD通常被认为具有主要优点。

  • 防御
    • 确保代码可能会更改,行为不会更改
    • 这允许进行如此重要的重构实践。
    • 您是否获得此TDD。
  • 设计
    • 在实施之前,您可以指定应该执行的操作,操作方式。
    • 这通常意味着更明智的实施决策
  • 文献资料
    • 测试套件应作为规范(要求)文档。
    • 为此目的使用测试意味着文档和实现始终处于一致状态-对一个进行更改意味着对另一个进行更改。与保留要求和在单独的Word文档上进行设计相比。

将责任与实施分开

作为程序员,非常容易想到将属性视为重要的事物,而将getter和setter视为某种开销。

但是属性是实现细节,而setter和getter是使程序真正起作用的契约接口。

拼写一个对象应该:

允许其客户更改其状态

允许其客户查询其状态

然后实际存储此状态的方式(对于该状态,属性是最常见的,但不是唯一的方式)。

诸如

(The Painter class) should store the provided colour

对于TDD 的文档部分很重要。

当您编写测试时,最终的实现是微不足道的(属性)并且没有任何防御优势的事实对您来说应该是未知的。

缺乏往返工程...

系统开发领域的关键问题之一是缺乏 往返工程1-系统的开发过程被分为多个相互分离的子过程,这些子过程的工件(文档,代码)通常不一致。

1 Brodie,MichaelL。“ John Mylopoulos:缝制概念模型的种子。” 概念建模:基础和应用程序。Springer Berlin Heidelberg,2009年。1-9。

...以及TDD如何解决

TDD 的文档部分可确保系统规范及其代码始终保持一致。

先设计,后实施

在TDD中,我们首先编写不合格的测试,然后再编写使他们通过的代码。

在更高级别的BDD中,我们首先编写方案,然后使它们通过。

为什么要排除设置器和获取器?

从理论上讲,在TDD中,完全有可能由一个人编写测试,而由另一个人来实现通过测试的代码。

因此,问问自己:

编写课程测试的人是否应该提及吸气剂和吸气剂。

由于getter和setter是类的公共接口,因此答案显然是肯定的,否则将无法设置或查询对象的状态。

显然,如果您首先编写代码,答案可能不会那么清晰。

例外情况

该规则有一些明显的例外-功能是明确的实现细节,并且显然不是系统设计的一部分。

例如,本地方法'B()':

function A() {

    // B() will be called here    

    function B() {
        ...
    }
} 

square()此处的私有函数:

class Something {
private:
    square() {...}
public:
    addAndSquare() {...}
    substractAndSquare() {...}
}

或不属于public接口的任何其他功能,需要在系统组件的设计中进行拼写。


1

遇到哲学问题时,请退回至驾驶要求。

您的目标是以具有竞争力的成本生产合理可靠的软件吗?

还是几乎不考虑成本就能生产出具有最高可靠性的软件?

在一定程度上,质量和开发速度/成本这两个目标是一致的:与编写缺陷相比,花费更少的时间编写测试。

但除此之外,他们没有。例如,每个开发人员每月报告一个错误并不难。将其减半至每两个月一次只会释放一两天的预算,而进行过多的测试可能不会使缺陷率减半。因此,这不再是简单的双赢。您必须根据客户的缺陷成本对其进行辩护。

这笔费用会有所不同(并且,如果您想作恶,那么他们通过市场或诉讼将这些费用强制退还给您的能力也将有所不同)。您不想作恶,因此您可以全额计算这些费用;有时,全球仍然存在一些考验,使世界变得更加贫穷。

简而言之,如果您试图盲目地将与客机飞行软件相同的标准应用于内部网站,那么您将最终破产或入狱。


0

您对此的答案取决于您的哲学(相信是芝加哥vs伦敦吗?我敢肯定有人会抬头)。陪审团仍未采用最省时的方法(因为毕竟这是最大的推动力-花费在修复上的时间更少)。

一些方法只测试公共接口,另一些方法则测试每个函数中每个函数调用的顺序。已经进行了大量的圣战。我的建议是尝试两种方法。选择一个代码单元,然后像X一样执行代码,然后像Y一样执行另一个代码。经过几个月的测试和集成后,请回去看看哪个代码更适合您的需求。


0

这是一个棘手的问题。

严格来讲,我认为这是没有必要的。您最好编写BDD样式的单元和系统级别的测试,以确保业务需求在肯定和否定的情况下都能按预期运行。

也就是说,如果您的方法未包含在这些测试用例中,那么您必须质疑为什么首先存在它,以及是否需要它,或者代码中是否存在未在您的文档或用户案例中反映的隐藏要求,即应该在BDD样式的测试用例中编码。

就我个人而言,我希望按行将覆盖率保持在85-95%左右,并将登机手续检入主线,以确保所有代码文件的每行现有单元测试覆盖率均达到此水平,并且不会发现任何文件。

假设正在遵循最佳测试实践,这将提供足够的覆盖范围,而不会迫使开发人员浪费时间试图仅仅为了覆盖而想出如何针对难于执行的代码或琐碎的代码获得更多的覆盖范围。


-1

问题本身就是问题,您不需要测试系统的所有功能就需要测试所有的“方法”或所有“类”。

它主要考虑功能/行为的术语,而不是方法和类。当然,这里提供了一种或多种功能的支持的方法,最后测试了所有代码,至少所有代码都与代码库有关。

在您的方案中,此“管​​理器”类可能是多余的或不必要的(就像名称中包含单词“ manager”的所有类一样),也可能不是,但看起来像一个实现细节,该类可能不值得使用进行测试,因为此类没有任何相关的业务逻辑。可能您需要该类才能使某些功能起作用,此功能的测试涵盖了该类,这样您可以重构该类并进行测试,以验证重要的事情(您的功能)在重构后仍然有效。

想想功能/行为而不是方法类,我不能重复足够的时间。


-4

不过,这让我思考。我们应该争取最高的测试覆盖率吗?

是的,理想情况下是100%,但是有些东西不能进行单元测试。

getter和setter(除非他们实际上有一些逻辑)

Getters / Setters是愚蠢的 -只是不要使用它们。而是将您的成员变量放在公共部分。

“样板”代码

获取通用代码,并对其进行单元测试。那应该就这么简单。

是否有任何合理/不“易燃”的理由来说明为什么要测试每一行(或尽可能多的行)代码?

否则,您可能会错过一些非常明显的错误。单元测试就像捕获某些错误的安全网一样,您应该尽可能多地使用它。

最后一件事:我在一个项目中,人们不想浪费时间为一些“简单代码”编写单元测试,但是后来他们决定根本不编写代码。最后,部分代码变成了泥泞的大球


好吧,让事情变得直截了当:我并不是说我不使用TDD / write测试。恰恰相反。我知道测试可能会发现我没有想到的错误,但是这里要测试什么?我只是简单地认为这种方法是“不可单元测试的”方法之一。正如PéterTörök所说(引用Kent Beck),您应该测试可能会损坏的东西。有什么可能在这里打破?并不是很多(此方法只有一个简单的委派)。我可以编写一个单元测试,但是它将仅具有DAO的模拟和一个断言,而无需太多测试。至于getter / setter,某些框架需要它们。
Zenzen 2012年

1
另外,由于我没有注意到它“请获取通用代码,并对其进行单元测试。这应该就这么简单。” 你是什​​么意思?它是一个服务类(在GUI和DAO之间的服务层中),对于整个应用程序是通用的。不能真正使它更通用(因为它接受一些参数并在DAO中调用某个方法)。唯一的原因就是要坚持应用程序的分层架构,因此GUI不会直接调用DAO。
Zenzen 2012年

20
-1表示“字母/字母很愚蠢-只是不要使用它们。相反,请将您的成员变量放在公共部分。” -错了 SO已经对此进行了多次 讨论。实际上,到处都使用公共场所甚至比到处都使用获取器和设置器更糟。
彼得Török
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.