Scala中如何测试私有类方法?

54

我有一个伴生对象,其中包含一个私有方法,如下所示:

package com.example.people

class Person(val age: Int)

object Person {
  private def transform(p: Person): Person = new Person(p.age + 1)
}

我想测试这种方法,例如使用以下内容:
class PersonSpec extends FlatSpec {

  "A Person" should "transform correctly" in {
    val p1 = new Person(1)
    val p2 = Person.transform(p1) // doesn't compile, because transform is private!
    assert( p2 === new Person(2) )
  }

}

如何让测试代码访问私有方法?

实际上,按照现有的写法,我可能能够创建 Person 的子类,但如果 Person 被声明为 finalsealed 呢?

谢谢!


3
将其设为包私有的,将测试代码放在同一包中,放在测试根目录下。 - om-nom-nom
5
单元测试用来测试类是否符合其契约。私有方法不是该契约的一部分,也不受类的不变量的限制。我认为私有方法不应该被(直接)测试。 - Randall Schulz
4
好的,同意。但是假设我有一个复杂的数据结构,而“transform”方法实现了该数据结构上的特定算法。我不想在API中公开该算法,但我需要它在每个角落案例中都能正常工作。为了能够全面测试它,我应该把“transform”方法放在哪里? - gdiazc
为什么该算法在API中不可用? - Randall Schulz
为什么不把 transform 方法作为 Person 类的公有方法呢?这不会泄露算法的任何抽象。 - Vidya
8个回答

89

在测试方面,我算是中庸的。通常情况下,我不会测试每一项内容,但有时候能够对私有函数进行单元测试非常有用,而且不需要破坏我的代码结构。如果你正在使用ScalaTest,你可以使用PrivateMethodTester来实现它。

import org.scalatest.{ FlatSpec, PrivateMethodTester }

class PersonSpec extends FlatSpec with PrivateMethodTester {

  "A Person" should "transform correctly" in {
      val p1 = new Person(1)
      val transform = PrivateMethod[Person]('transform)
      // We need to prepend the object before invokePrivate to ensure
      // the compiler can find the method with reflection
      assert(p2 === p1 invokePrivate transform(p1))
    }
  }

这可能不完全是你想要做的,但你能理解这个想法。


14
import org.scalatest.{ FlatSpec, PrivateMethodTester } 是需要导入的内容。我讨厌去寻找这些东西,而且我有时也会忘记在答案中添加它。 - smashedtoatoms
7
在我意识到在 invokePrivate 前应该添加类名之前,代码对我来说并没有起作用,即 Person invokePrivate transform(p1) - Dotan
这里有一个来自scalaTest文档的例子,它将我引导到PrivateMethodTester。 - mahmoud mehdi
2
如何针对一个 object 做同样的事情? - Wonay
每次都会给我一个“java.lang.IllegalArgumentException: Can't find a private method named: xy”的错误。 - beatrice
显示剩余9条评论

40

您可以将方法声明为包私有:

private[people] def transform(p: Person): Person = new Person(p.age + 1)
如果你将PersonSpec放在同一个包中,它就能访问它。
我让你决定是否明智地对私有方法进行单元测试 :)

2
从技术上讲当然是正确的,但我认为仅仅为了使测试更容易(而不是为了满足客户需求)而扩大访问范围是一个坏主意,并且暗示着某些问题存在。在这个意义上,我有点接近@Peter的设计气味立场。 - Vidya
@Vidya SO旨在提供技术问题/答案的平台,其他StackExchange网站则旨在提供设计/最佳实践问题的解答。 - vptheron
3
如果问题是“我是否应该测试私有方法?”那么这是正确的。在这种情况下,提问者基于一个许多人认为有缺陷的前提提出了一个真正的技术问题,指出这一点是公平的。毕竟,解决问题的最好方式是防止它发生。我还将指出 这个 SO 主题相同的讨论 被认为非常有价值,因此被保存以备后用。因此,这样的讨论是有先例的。 - Vidya
1
@Vidya,这个问题并没有被归档,并且是在 SO 的早期(2008 年)提出的,当时管理和站点范围还不是那么严格,因此这不是一个优先权的好论据。另请参见有关历史锁定的元数据。 - om-nom-nom
那个帖子中有许多2012年的条目,所以我不知道你从哪里得出了2008年。这就像是整整一个美国总统任期的差异。或者是奥运会。无论如何,请启迪我。SO何时才从《饥饿游戏》(http://www.imdb.com/title/tt1392170/)的无拘无束混乱转变为《伊利亚特》(http://www.imdb.com/title/tt1535108/)的严格纪律?我会确保只引用从那一点开始的帖子。 - Vidya
通常我不需要测试私有方法,但当我需要模拟一些被其他正在测试的方法使用的私有方法中的功能时,这是很有帮助的。 - Lilith Schneider

5
需要对私有方法进行单元测试是一种设计问题的信号。如果私有方法很小且只是帮助方法,则可以通过公共API进行测试,这样就可以了。否则,更可能的情况是它包含不同的逻辑/职责,并且应该将其移动到另一个类中,该类通过代理在Person中使用。然后,您将首先测试该类的公共API。相关回答中提供了更多详细信息。
可能可以使用Java / Scala反射访问它,但这只是解决设计问题的权宜之计。如果需要,请参见相关Java回答以了解如何做到这一点

61
如果你不测试私有方法,我认为这是代码异味。单元测试应该测试应用程序中最小的工作单元。你所描述的更像是针对API的验收测试。你不应该通过公共API来测试私有方法。 - wizulus
私有方法应该可以进行单元测试。私有可见性是为了封装服务的,而单元测试则是其中非常重要的一部分。 - Coder Guy
有趣的是,这个答案一次又一次地被踩,正如alancnet的评论所指出的那样。 - Peter Kofler
4
单元测试对于开发人员在修改代码时非常有用。帮助您的信息并不是传递给API更高级别的聚合通过/失败条件,而是帮助您理解正在更改的方法完全局限于方法的本地信息,并且应该在测试中包含。虽然“只需在公共API中进行测试”对于传递的聚合业务问题是可以的,但对于日常开发和密集的代码更改来说,则一点也不好。反馈循环对于为什么私有方法中的本地更改会破坏测试的原因不够信息化。 - ely
我同意这个回答。如果你需要测试私有方法,那就意味着你正在编写具有过多职责的类。将其拆分为更小的组件,这些组件自然可测试。 - kiedysktos

4

@jlegler的答案对我有所帮助,但在使事情正常工作之前,我仍然需要进行一些调试,因此我想在这里写出确切需要的内容。

测试:

class A

object A {
  private def foo(c: C): B = {...}
}

使用:

val theFuncion = PrivateMethod[B]('foo)
val result = A invokePrivate theFunction(c)

请注意A、B的位置。

3

个人认为所有内容都应该公开,只需在前面添加___表示其他开发人员不应使用它。

我知道这是Scala而不是Python,但无论如何,"我们都是成年人"。

“私有”方法实际上并不是私有的(例如此处),并且肯定不安全,因此为什么要为基本上是社会契约的事情增加困难?只需添加前缀即可 - 如果另一个开发人员想要深入探究,他们要么有充分理由,要么就该承担后果。


1
这个组织值得吗?将某些东西私有化可以保护一个开发者的错误决策不会影响到整个组织,因为它可以防止该开发者首先做出选择。 - Brian Yeh

2

一般而言,如果你想有效地测试你的代码,你首先必须编写可测试的代码。

Scala实现了函数式范式,并通过设计广泛使用不可变对象,“case类”就是一个例子(我的观点是:Person类应该是一个case类)。

实现私有方法是有意义的,如果对象具有可变状态,在这种情况下,您可能希望保护对象的状态。但如果对象是不可变的,为什么要将方法实现为私有的呢?在您的示例中,该方法生成Person的副本,出于什么原因您想将其设为私有?我没有看到任何理由。

我建议你考虑一下这个问题。再次强调,如果你想有效地测试你的代码,你必须编写可测试的代码。


在你的例子中,该方法生成了一个Person的副本,你为什么想要将其设为私有?以免在用户API中添加他不感兴趣的方法。 - MaxNevermind

1
一个可能的解决方法是间接测试私有方法:测试调用私有方法的公共方法。

1
我认为单元测试并非测试类的合约,而是测试简单功能(单元)。
此外,我认为仅为了方便测试而将某些方法公开并不是一个好主意。我相信尽可能保持API狭窄是帮助其他开发人员使用您的代码(IDE不会建议私有方法)并理解合约的好方法。
同时,我们不应该把所有东西都放在一个方法中。因此,有时我们可以将一些逻辑放入一个私有方法中...当然,我们也想测试它。通过公共API测试它将增加测试的复杂性。(另一种选择是将私有方法的逻辑移动到另一个辅助类中并在那里进行测试...这个类不会直接被开发人员使用,也不会使API混乱)
我认为Scalatest团队添加了PrivateMethodTester是有目的的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接