私有/受保护的方法是否应该接受单元测试?


82

在TDD开发中,通常要做的第一件事是创建接口,然后开始针对该接口编写单元测试。随着TDD过程的进行,您最终将创建一个实现该接口的类,然后在某个时候您的单元测试将通过。

现在,我的问题是关于私有和受保护的方法,我可能必须在类中编写这些方法以支持接口公开的方法/属性:

  • 类中的私有方法是否应该具有自己的单元测试?

  • 类中的受保护方法是否应具有自己的单元测试?

我的想法:

  • 特别是因为我正在对接口进行编码,所以我不必担心受保护/私有方法,因为它们是黑盒。

  • 因为我使用的是接口,所以我正在编写单元测试以验证所定义的约定是否由实现该接口的不同类正确实现,因此我也不必担心私有/受保护的方法,而应该通过调用接口定义的方法/属性。

  • 如果我的代码覆盖率未显示出受保护的/私有方法被击中,则我没有正确的单元测试,或者我的代码未被使用,应删除。


1
如果您不通过覆盖测试或调用它们来对测试使用受保护的方法,那么为什么要对它们进行保护而不是私有?通过使它们受到保护,您将有意识地决定公开扩展点/功能。对我来说,如果您遵循TDD,则此决定应由您编写的测试决定。
forsvarir 2011年

2
您应该将有关自己想法的部分放在单独的答案中。让我知道您什么时候会投票。
基思·品森


您对活动的单元测试(即设置为连续运行的单元测试)是正确的。对于这些,您只希望测试公共和受保护的接口。您也可以从为私有方法编写测试中受益。这些测试不应该是连续套件的一部分,但是作为一次检验您的实现是否良好的工具,它可能是非常有价值的工具。
Didier A.

Answers:


108

不,我不认为要测试私有或受保护的方法。类的私有方法和受保护方法不是公共接口的一部分,因此它们不公开公共行为。通常,这些方法是通过将测试变为绿色后所应用的重构来创建的。

因此,这些私有方法由声明公共接口行为的测试隐式测试。

从哲学的角度讲,请记住,您是在测试行为,而不是方法。因此,如果您想到被测类可以做的事情,只要您可以测试并断言该类的行为符合预期,则该类内部是否使用私有(和受保护)方法来实现这种行为是无关紧要的。这些方法是公共行为的实施细节。


23
我喜欢您说单元测试,测试行为而不是方法的事实!这使事情变得非常清晰。
Raj Rao

1
我同意@rajah。那应该是每个教程的第一句话。我一直想知道如何测试我的方法,现在我知道我不需要这样做。+1
frostymarvelous

3
如果基类实现了希望公众继承和使用的受保护行为,您是否还说这适用?然后,受保护的方法仍然是公共接口的一部分,不是吗?
尼克·乌德尔2014年

1
一般而言,有利于关注点分离的模式更适合于隔离的单元测试,而有利于封装的模式则更易于使用API​​。
尤川豪2015年

3
这并不能消除可见性受保护的情况。似乎受保护的方法也是接口的一部分,通常,这是一个扩展点,故意将其保护为此类。我想在这种情况下,您还应该对它们进行单元测试。您不希望任何人将来改变事物并破坏依赖那些扩展点的行为的类。
Didier A.

45

我不同意大多数海报。

最重要的规则是:工作代码敲打有关公共/保护/私有的理论规则。

您的代码应经过全面测试。如果您可以通过编写针对公共方法的测试来达到目标​​,那么就可以充分利用受保护/私有方法,那就太好了。

如果不能,则可以重构以便可以,或者修改受保护/专用规则。

关于一位心理学家给孩子们做过测试的故事很有趣。他给每个孩子两个木板,木板的两端都系着绳子,并要求他们越过一个房间,不要脚踩到地板上。所有的孩子都用木板像小雪橇一样,每只脚踩一只脚,用绳子抓住它们,然后在地板上滑动。然后他给了他们同样的任务,但是只用了一块木板。他们在地板上枢转/“行走”,在单板的每一端都用了一只脚-他们更快!

仅仅因为Java(或任何语言)具有(私有/受保护/公共)功能并不意味着您正在使用它来编写更好的代码!

现在,将始终存在优化/最小化此冲突的方法。在大多数语言中,可以将方法设置为受保护的方法(而不是公共方法),并将测试类放入同一程序包(或其他程序包)中,该方法将可用于测试。如其他张贴者所述,有一些注释可以提供帮助。您可以使用反射来获取私有方法(糟糕)。

上下文也很重要。如果您要编写供外部人员使用的API,则公共/私有更为重要。如果是内部项目,谁在乎?

但是,归根结底,请考虑一下由于缺乏测试而导致了多少错误。然后比较“过分可见”的方法引起了多少错误。该答案将推动您做出决定。


3
如果方法很关键,并且逻辑复杂,则断言其行为对于防止错误很有用。为这种方法编写单元测试甚至可以帮助您以某种探索性的方式实现该方法。因此,即使它是私有的,我也会说值得进行单元测试。但是,还有很多,但是,您必须记住测试是代码耦合的。如果对方法编写测试,则将阻止重构。
Didier A.

6
因此,在开始为私有方法编写测试之前,我想说的是总是重新考虑您的设计。查看事物是否可以被概括并转化为纯函数方法。如果是这样,则可以将它们提取到自己的构造中。然后,此构造可以具有自己的公共接口并进行单元测试。请记住,通常,私有方法中的复杂行为可能表示类承担的责任不止一种。因此,请先重新考虑您的设计。
Didier A.

是的,但是什么是“工作代码”?测试私有方法并不能说明您的对象是否具有正确的行为。这就是为什么我们只测试公共方法的要点。只有公共方法才能表现出一段代码用户所关心的行为。
Sammi

1
“工作代码”是有效的代码。如果您的私有(或准私有)方法中存在一个错误,而您的公共方法的测试未发现该错误,则说明存在问题。也许您的设计是错误的,很公平:我同意最好的解决方案是调用公共方法的测试。但这并非总是可能的,尤其是当您要添加或修复旧代码时。(根据我的经验,在一个具有一百万行代码的项目中。)在一段时间内,经过测试的代码总是比未经测试的代码更好。即使我们打破了仅测试公共方法的好规则!
查尔斯·罗斯

“测试是代码耦合...防止重构”这一点(顶部)是100%错误。在建筑的隐喻中,测试是脚手架,而不是具体的。事情改变了,测试改变了,被扔掉了,新的测试被编写了。我同意好的设计可以最大程度地减少测试重写。但是,即使是最好的设计也会发生变化。
罗斯

34

你写了:

在TDD开发中,通常要做的第一件事是创建接口,然后开始针对该接口编写单元测试。随着TDD过程的进行,您最终将创建一个实现该接口的类,然后在某个时候您的单元测试将通过。

请让我用BDD语言改写一下:

在描述一个类为何有价值的原因及其行为方式时,通常要做的第一件事是创建一个如何使用该类的示例,通常通过其接口*。添加所需的行为时,最终会创建一个提供该值的类,然后在某些时候您的示例起作用。

*可以是Interface该类的实际或简单的可访问API,例如:Ruby没有接口。

这就是为什么您不测试私有方法的原因-因为测试是如何使用该类的示例,因此您实际上不能使用它们。如果要执行的操作是将私有方法中的职责委派给协作类,然后对辅助程序进行模拟/存根。

对于受保护的方法,您是说扩展您的类的类应具有某些特定的行为并提供某些值。然后,您可以使用类的扩展来演示该行为。例如,如果您正在编写一个有序的集合类,则可能要证明两个具有相同内容的扩展名具有相等性。

希望这可以帮助!


1
辉煌的职位。澄清了很多。
frostymarvelous 2011年

17

在为类编写单元测试时,不必关心类的功能是否直接在公共接口上的方法中实现,还是在一系列私有方法中实现。因此,是的,您应该测试自己的私有方法,但是您不必为此直接从测试代码中调用它们(直接测试私有方法会将您的实现紧密地耦合到您的测试中,并使重构变得不必要地困难)。

受保护的方法在您的班级与其将来的子级之间形成了不同的合同,因此,您实际上应该在与公共接口相似的程度上对其进行测试,以确保正确定义和执行合同。


13

没有!仅测试接口。

TDD的一大好处是,无论您选择如何实现私有方法,都确保该接口有效。


11

完成上面其他人所说的,我要说的是,受保护的方法是某种接口的一部分:它恰好是暴露于继承而不是继承的方法,这是每个人在考虑接口时都倾向于考虑的问题。

将方法标记为受保护而不是私有意味着该方法预期将由第三方代码使用,因此需要定义和测试某种合同,就像由公共方法定义的普通接口一样,它们都可以继承和组合。


9

编写测试有两个原因:

  1. 声明预期行为
  2. 防止行为回归

承担(1)声明预期行为:

在断言预期的行为时,您要确保代码按预期的方式工作。这实际上是一种自动化的方式,可以执行例行验证,任何开发人员在实施任何类型的代码时都会执行以下操作:

  • 我刚刚写的作品行得通吗?
  • 这个循环真的结束了吗?
  • 它按我认为的顺序循环吗?
  • 这对空输入有效吗?

这些都是我们所有人都会想到的问题,通常,我们也会尝试在脑海中执行代码,确保它看起来确实有效。对于这些情况,让计算机以确定的方式回答这些问题通常很有用。因此,我们编写了一个断言确实有效的单元测试。这使我们对代码充满信心,可以帮助我们及早发现缺陷,甚至可以帮助实际实施代码。

在需要的地方进行此操作是个好主意。任何难以理解的代码,或非琐碎的代码。即使是平凡的代码也可以从中受益。这完全取决于您自己的信心。多久做一次和走多远取决于您自己的满意程度。当您可以自信地回答“是”时,请停下来:您确定这样做有效吗?

对于这种测试,您不需要关心可见性,界面或其他任何方面,而只关心拥有有效的代码。因此,是的,如果您认为需要对它们进行测试以回答问题的“是”,则可以测试私有和受保护的方法。

承担(2)防止行为回归:

一旦获得了有效的代码,就需要有一种机制来保护该代码免受将来的损害。如果没有人再次接触您的源和配置,则不需要此操作,但是在大多数情况下,您或其他人将接触您的源和软件的配置。这种内部麻烦很可能破坏您的工作代码。

大多数语言中已经存在机制来防止这种损坏。可见性功能是一种机制。私有方法被隔离并隐藏。封装是另一种机制,您可以对事物进行分隔,因此更改其他分隔不会影响其他分隔。

通用的机制称为:边界编码。通过在代码各部分之间创建边界,可以保护边界内的所有内容免受其外部内容的影响。边界成为相互作用的点,以及事物相互作用的契约。

这意味着对边界的更改(通过破坏其接口或破坏其预期的行为)将损坏并可能破坏依赖于该边界的其他边界。这就是为什么有一个单元测试的好主意,它针对这些边界并断言它们在语义和行为上都没有改变。

这是您的典型单元测试,是大家在提及TDD或BDD时谈论的话题。重点是加强边界并保护它们不受更改。您不想为此测试私有方法,因为私有方法不是边界。受保护的方法是受限制的边界,我会保护它们。它们没有暴露于世界,但是仍然暴露于其他隔间或“单位”。

该怎么做?

正如我们所看到的,有充分的理由对公共方法和受保护方法进行单元测试,以断言我们的接口没有改变。而且有充分的理由测试私有方法,以断言我们的实现工作。那么我们应该对它们全部进行单元测试吗?

是和否

首先:测试所有您认为需要确定性证明的方法,以便能在大多数情况下确保您的代码有效,无论可见性如何。然后,禁用这些测试。他们已经完成了工作。

最后:为您的边界编写测试。对将由系统其他单元使用的每个点进行单元测试。确保此测试声明语义约定,方法名称,参数数量等。还确保测试声明单元的可用行为。您的测试应演示如何使用该设备以及该设备可以做什么。使这些测试保持启用状态,以便它们在每次代码推送时都运行。

注意:禁用第一组测试的原因是为了允许进行重构工作。主动测试是代码耦合。它可以防止将来修改正在测试的代码。您只需要为您的界面和交互合同使用。


1
听起来好像您没有显式地单独测试私有方法,它们不在您的测试范围内,并且您不相信它们可以工作。我声称这是完全错误的。无法通过公共方法测试的私有方法(或其中的任何代码路径)为无效代码,应将其删除。TDD的全部目的是仅通过测试公共方法来获得完全覆盖,因为您编写了不存在的0 LoC来进行测试。测试隔离的私有方法只能使重构变得更加困难,这与TDD目标(其中之一)正好相反。
sara 2015年

@kai我明确指出,您不应该对私有方法进行自动测试,但是有时使用隔离测试来帮助您实施很有价值。这些测试不应成为测试套件的一部分,或者由于您提到的原因(即重构)而应被禁用。根据您自己的信心水平来决定何时选择进行程序测试来实现私有方法。也许直到我回答结束才读?
Didier A.

您声称“还有充分的理由测试私有方法,以断言我们的实现工作”。我在帖子中没有找到任何依据。测试私有方法并不能告诉您有效的实现,而测试公共方法也不能告诉您。私有方法有效或无效。如果它不起作用,它将测试一个或多个公共方法失败或代码失效和/或未经测试。
sara 2015年

@kai您提到:“或者它已失效和/或未经测试的代码”。我正在谈论未经测试的代码。私有方法可能会从公共方法中隐藏很多没有执行边缘情况的错误。想象一下一个错误关闭。有时,公共方法的不变量使之成为现实,因此这种情况将永远不会发生。在这种情况下,我认为私有方法仍然有问题,并且实现有缺陷,但是,其集成防止发现和捕获错误。在这种情况下,您可能希望进行一些测试以尝试极端情况,以便可以确保您的方法没有错误。
Didier A.

@kai但是,请理解,我正在谈论的那些测试不是您的测试套件,这不是TDD。我的意思是,如果您可以针对它们快速运行一些测试,则可以简化一些私有方法的实现。在具有REPL的语言中,您不需要那么多。您可以尝试逐步了解该方法,但我建议您仅对难以实现的私有方法进行计算机运行测试。我建议事后删除测试,或使其保持禁用状态,或在未在CI生成中运行的特殊位置保留测试。
Didier A.

4

不,您不应该测试私有方法(无论如何,如果不使用诸如反射之类的可怕方法,您将如何进行测试)。使用受保护的方法,在C#中不太明显,您可以使内部受保护,我认为这样做可以测试通过模板模式方法实现其所有功能的派生类。

但是,总的来说,如果您认为您的公共方法做得太多,那么是时候将您的类重构为更多的原子类,然后测试这些分类了。


2

我也同意@kwbeam关于不测试私有方法的回答。但是,我想强调一个重要的点-受保护的方法是类的导出API的一部分,因此必须进行测试。

受保护的方法可能无法公开访问,但是您绝对可以为子类提供一种使用/覆盖它们的方法。类之外的东西可以访问它们,因此您需要确保那些受保护的成员以预期的方式行为。因此,不要测试私有方法,而要测试公共方法和受保护的方法。

如果您认为自己有一个包含关键逻辑的私有方法,则可以尝试将其提取到一个单独的对象中,将其隔离并提供一种测试其行为的方法。

希望能帮助到你!


2

如果您的目标是提高代码覆盖率(我建议您这样做),则应测试所有方法,无论它们是私有的还是受保护的。

受保护是一种不同的讨论点,但总而言之,它根本不应该存在。要么破坏对已部署代码的封装,要么迫使您从该类继承,仅对其进行单元测试,甚至有时您不需要继承。

仅仅向客户隐藏一个方法(私有化)并不能赋予它不被审核的特权。因此,可以通过前面提到的公共方法来测试它们。


1

我同意其他所有人的观点:您的问题的答案是“否”。

确实,您完全正确地对待了您的方法和想法,尤其是在代码覆盖率方面。

我还要补充一点,这个问题(和答案为“否”)也适用于您可能会引入类的公共方法。

  • 如果您添加方法(公共/保护或私有)是因为它们通过了失败的测试,那么您或多或少已经实现了TDD的目标。
  • 如果您只是因为违反TDD而决定添加方法(公共/受保护的或私有的),那么您的代码覆盖率应该会抓住这些问题,并且您应该能够改善流程。

另外,对于C ++(我应该只考虑C ++),我仅使用私有方法来实现接口,以指示仅应通过其实现的接口来使用该类。这避免了我错误地从测试中调用添加到实现中的新方法


0

好的设计意味着将应用程序分成多个可测试的单元。完成此操作后,某些单元将公开使用公共API,而另一些可能不会。同样,暴露单元与这些“内部”单元之间的交互点也不是公共API的一部分。

我认为,一旦有了可识别的单元,无论是否通过公共API公开,都将受益于单元测试。

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.