我已经阅读了这篇关于如何测试私有方法的文章。我通常不测试它们,因为我总是认为只测试将从对象外部调用的公共方法更快。你测试私有方法吗?我应该一直测试它们吗?


我认为单元测试是用来测试公共方法的。您的公共方法使用您的私有方法,因此它们也间接地接受测试。


如果您不测试您的私有方法,您怎么知道它们不会崩溃呢?


我不对私有方法进行单元测试。私有方法是应该对类的用户隐藏的实现细节。测试私有方法会破坏封装。

如果我发现私有方法非常庞大、复杂或重要到需要自己的测试,我就把它放在另一个类中,并在那里将它设为公共(方法对象)。然后,我可以轻松地测试以前是私有但现在是公共的方法,该方法现在存在于它自己的类中。


我认为最好只测试对象的公共接口。从外部世界的角度来看,只有公共接口的行为是重要的,这是您的单元测试应该指向的方向。

一旦你为一个对象编写了一些可靠的单元测试,你就不希望因为接口背后的实现改变了而不得不回去修改那些测试。在这种情况下,您已经破坏了单元测试的一致性。


我不是这方面的专家,但是单元测试应该测试行为,而不是实现。私有方法严格来说是实现的一部分,所以不应该测试IMHO。


这显然与语言有关。在过去的c++中,我将测试类声明为友类。不幸的是,这需要您的生产代码了解测试类。


我们通过推断来测试私有方法,我的意思是我们寻找总类测试覆盖率至少为95%,但我们的测试只调用公共方法或内部方法。为了获得覆盖,我们需要根据可能发生的不同场景对公众/内部人员进行多次调用。这使得我们的测试更专注于它们所测试的代码的目的。

特朗皮对你链接的帖子的回答是最好的。


我倾向于遵循Dave Thomas和Andy Hunt在他们的书《实用单元测试》中的建议:

一般来说,为了不破坏任何封装 测试(或者就像妈妈常说的,“不要暴露你的私处!”)。大多数 此时,您应该能够通过执行类的 公共方法。如果隐藏了重要的功能 在私人或受保护访问的背后,这可能是一个警告信号 还有一群人在里面挣扎着想出来。

但有时我无法阻止自己测试私有方法,因为它给了我一种安全感,我正在构建一个完全健壮的程序。


如果你的私有方法没有通过调用公共方法来测试,那么它在做什么? 我说的是私人的,不是受保护的,也不是朋友。


如果私有方法定义良好(即,它有一个可测试的函数,并且不会随着时间的推移而改变),那么是的。我在有意义的地方测试所有可测试的东西。

例如,加密库可能会隐藏这样一个事实,即它使用一次仅加密8个字节的私有方法执行块加密。我会为此写一个单元测试——它并不意味着要改变,即使它是隐藏的,如果它真的坏了(例如,由于未来的性能增强),那么我想知道是私有函数坏了,而不仅仅是某个公共函数坏了。

它加快了以后的调试。

亚当


当我在我们的项目中越来越多地遵循我们最新的QA建议时,我感觉有必要测试私有函数:

每个函数圈复杂度不超过10。

现在,执行这项政策的副作用是,我的许多非常大的公共职能被划分为许多更集中的,更好地命名为私人职能。 公共功能仍然存在(当然)但本质上被简化为所有那些私人的“子功能”

这实际上很酷,因为调用堆栈现在更容易阅读(而不是一个大函数中的bug,我在一个子子函数中有一个bug,它具有调用堆栈中先前函数的名称,以帮助我理解“我是如何到达那里的”)

然而,现在似乎更容易直接对那些私有函数进行单元测试,而将大型公共函数的测试留给某种需要解决场景的“集成”测试。

这只是我的个人意见。


测试的目的是什么?

到目前为止,大多数答案都说私有方法是实现细节,只要公共接口经过良好测试并能够正常工作,这些实现细节就不重要(至少不应该)。如果测试的唯一目的是保证公共接口正常工作,那么这是绝对正确的。

就我个人而言,我对代码测试的主要用途是确保将来的代码更改不会导致问题,并且在出现问题时帮助我进行调试。我发现对私有方法的测试就像对公共接口的测试一样彻底(如果不是更彻底的话!),可以进一步达到这个目的。

考虑:您有一个公共方法A,它调用私有方法B。A和B都使用方法C。C被更改(可能由您更改,也可能由供应商更改),导致A开始测试失败。对B进行测试不是很有用吗,即使它是私有的,这样你就知道问题是在A使用C, B使用C,还是两者都有?

Testing private methods also adds value in cases where test coverage of the public interface is incomplete. While this is a situation we generally want to avoid, the efficiency unit testing depends both on the tests finding bugs and the associated development and maintenance costs of those tests. In some cases, the benefits of 100% test coverage may be judged insufficient to warrant the costs of those tests, producing gaps in the public interface's test coverage. In such cases, a well-targeted test of a private method can be a very effective addition to the code base.


如果方法足够重要/复杂,我通常会让它“受保护”并测试它。一些方法将保持私有,并作为公共/受保护方法的单元测试的一部分进行隐式测试。


正如上面引用的,“如果你不测试你的私有方法,你怎么知道它们不会崩溃?”

这是一个大问题。单元测试的一个重要要点是尽快知道在哪里、何时以及如何发生故障。这样就减少了大量的开发和QA工作。如果所有测试的都是公开的,那么您就没有对类的内部进行诚实的覆盖和描述。

我发现最好的方法之一是将测试引用添加到项目中,并将测试放在与私有方法并行的类中。放入适当的构建逻辑,这样测试就不会构建到最终项目中。

然后,您就有了测试这些方法的所有好处,您可以在几秒钟内而不是几分钟或几小时内发现问题。

总之,单元测试你的私有方法。


如果您正在开发测试驱动(TDD),您将测试您的私有方法。


如果我发现私有方法非常庞大、复杂或重要到需要自己的测试,我就把它放在另一个类中,并在那里将它设为公共(方法对象)。然后,我可以轻松地测试以前私有但现在是公共的方法,它现在存在于自己的类中。


是的,我确实测试私有函数,因为尽管它们是由你的公共方法测试的,但在TDD(测试驱动设计)中测试应用程序的最小部分是很好的。但是在测试单元类中不能访问私有函数。下面是我们测试私有方法的方法。

为什么我们有私有方法?

私有函数主要存在于我们的类中,因为我们希望在公共方法中创建可读的代码。 我们不希望这个类的用户直接调用这些方法,而是通过我们的公共方法。此外,我们不希望在扩展类时改变它们的行为(在受保护的情况下),因此它是一个private。

当我们编码时,我们使用测试驱动设计(TDD)。这意味着有时我们会偶然发现一个私有的功能片段并想要进行测试。私有函数在phpUnit中是不可测试的,因为我们不能在Test类中访问它们(它们是私有的)。

我们认为有3个解决方案:

1. 你可以通过你的公共方法来测试你的私处

优势

简单的单元测试(不需要“hack”)

缺点

程序员需要了解公共方法,而他只想测试私有方法 您不是在测试应用程序中最小的可测试部分

2. 如果private是如此重要,那么为它创建一个新的单独的类可能是一个代码味道

优势

你可以把它重构成一个新类,因为如果是这样的话 重要的是,其他类可能也需要它 可测试单元现在是一个公共方法,因此可测试

缺点

如果一个类是不需要的,并且只被 方法来自的类 由于增加的开销而造成潜在的性能损失

3.将访问修饰符更改为(final) protected

优势

您正在测试应用程序中最小的可测试部分。当 使用final protected,函数将不会被重写(只是 像一个私人) 无性能损失 没有额外的开销

缺点

你把一个私有访问权限改成了受保护,也就是说 它的孩子可以接触到 您仍然需要在测试类中使用Mock类

例子

class Detective {
  public function investigate() {}
  private function sleepWithSuspect($suspect) {}
}
Altered version:
class Detective {
  public function investigate() {}
  final protected function sleepWithSuspect($suspect) {}
}
In Test class:
class Mock_Detective extends Detective {

  public test_sleepWithSuspect($suspect) 
  {
    //this is now accessible, but still not overridable!
    $this->sleepWithSuspect($suspect);
  }
}

因此,我们的测试单元现在可以调用test_sleepWithSuspect来测试之前的私有函数。


绝对是的。这就是单元测试的要点,你测试单元。私有方法是一个单元。没有测试私有方法,TDD(测试驱动开发)是不可能的,


我已经为这个问题苦恼了一段时间,尤其是在尝试TDD的时候。

我曾经看到过两篇文章,我认为它们在TDD的情况下已经足够彻底地解决了这个问题。

测试私有方法,TDD和测试驱动重构 测试驱动开发不是测试


总而言之:

当使用测试驱动开发(设计)技术时,私有方法应该只在重构已经工作和测试过的代码的过程中出现。 根据这个过程的本质,任何从经过彻底测试的功能中提取出来的简单实现功能都将是自我测试的(即间接测试覆盖率)。

对我来说,在编码的开始部分,大多数方法都是高级函数,因为它们封装/描述了设计。

因此,这些方法将是公开的,并且测试它们将非常容易。

私有方法将在一切正常工作后出现,并且为了可读性和清洁度我们正在进行重构。


I understand the point of view where private methods are considered as implementations details and then don't have to be tested. And I would stick with this rule if we had to develop outside of the object only. But us, are we some kind of restricted developers who are developing only outside of objects, calling only their public methods? Or are we actually also developing that object? As we are not bound to program outside objects, we will probably have to call those private methods into new public ones we are developing. Wouldn't it be great to know that the private method resist against all odds?

我知道有些人会回答说,如果我们正在开发另一个公共方法到那个对象中,那么这个方法应该被测试,就是这样(私有方法可以在没有测试的情况下继续存在)。但这也适用于一个对象的任何公共方法:当开发一个web应用程序时,一个对象的所有公共方法都是从控制器方法调用的,因此可以认为是控制器的实现细节。

那么为什么我们要单元测试对象呢?因为这是非常困难的,不是说不可能,以确保我们正在测试控制器的方法与适当的输入将触发底层代码的所有分支。换句话说,我们在堆栈中的位置越高,测试所有行为就越困难。私有方法也是如此。

对我来说,私人方法和公共方法之间的界限是测试时的心理标准。对我来说更重要的标准是:

该方法是否在不同的地方被多次调用? 这种方法是否复杂到需要测试?


你不应该这么做。如果您的私有方法有足够的复杂性,必须进行测试,您应该将它们放在另一个类中。保持高内聚性,类应该只有一个目的。类公共接口应该足够了。


我看到很多人都有同样的想法:在公共层面上进行测试。但这不正是我们的QA团队所做的吗?他们测试输入和预期输出。如果作为开发人员,我们只测试公共方法,那么我们只是在重做QA的工作,而不是通过“单元测试”来增加任何价值。


其中一个要点是

如果我们测试以确保逻辑的正确性,并且私有方法携带逻辑,那么我们应该测试它。不是吗?我们为什么要跳过这个呢?

基于方法的可见性编写测试是完全无关的想法。

相反

另一方面,在原始类之外调用私有方法是一个主要问题。在一些模拟工具中,模拟私有方法也有局限性。(例:5)

虽然有一些工具如Power Mock支持这一点,但这是一个危险的操作。原因是它需要破解JVM来实现这一点。

一种可以解决的方法是(如果您想为私有方法编写测试用例)

将这些私有方法声明为受保护的。但在某些情况下可能不方便。


“我应该测试私有方法吗?”的答案是“有时.......”。通常情况下,您应该针对类的接口进行测试。

其中一个原因是您不需要对一个特性进行双重覆盖。 另一个原因是,如果您更改了私有方法,则必须为它们更新每个测试,即使对象的接口根本没有更改。

这里有一个例子:

class Thing
  def some_string
    one + two
  end

  private 

  def one
    'aaaa'
  end

  def two
    'bbbb'
  end

end


class RefactoredThing
def some_string
    one + one_a + two + two_b
  end

  private 

  def one
    'aa'
  end

  def one_a
    'aa'
  end

  def two
    'bb'
  end

  def two_b
    'bb'
  end
end

在RefactoredThing中,你现在有5个测试,其中2个你必须为重构而更新,但你的对象的功能实际上没有改变。所以让我们假设事情比这更复杂,你有一些方法来定义输出的顺序,比如:

def some_string_positioner
  if some case
  elsif other case
  elsif other case
  elsif other case
  else one more case
  end
end

这不应该由外部用户来运行,但是您的封装类可能太笨重了,无法一遍又一遍地运行这么多逻辑。在这种情况下,您可能更愿意将其提取到一个单独的类中,为该类提供一个接口并对其进行测试。

最后,假设你的主对象非常重,方法非常小你需要确保输出是正确的。你会想,“我必须测试这个私有方法!”也许你可以通过传入一些繁重的工作作为初始化参数使你的对象更轻?然后你可以放一些更轻的东西进去测试。


不,你不应该测试私有方法,为什么?此外,流行的mock框架(如Mockito)不支持测试私有方法。


出于几个原因,我不喜欢测试私有功能。它们如下(这些是TLDR人员的主要观点):

Typically when you're tempted to test a class's private method, it's a design smell. You can test them through the public interface (which is how you want to test them, because that's how the client will call/use them). You can get a false sense of security by seeing the green light on all the passing tests for your private methods. It is much better/safer to test edge cases on your private functions through your public interface. You risk severe test duplication (tests that look/feel very similar) by testing private methods. This has major consequences when requirements change, as many more tests than necessary will break. It can also put you in a position where it is hard to refactor because of your test suite...which is the ultimate irony, because the test suite is there to help you safely redesign and refactor!

我将用一个具体的例子来解释这些问题。事实证明,2)和3)之间存在某种复杂的联系,因此它们的示例类似,尽管我认为它们是不应该测试私有方法的不同原因。

有时测试私有方法是合适的,只是重要的是要意识到上面列出的缺点。我稍后会更详细地讨论它。

我还讨论了为什么TDD不是在最后测试私有方法的有效借口。

重构你摆脱糟糕设计的方法

One of the most common (anti)paterns that I see is what Michael Feathers calls an "Iceberg" class (if you don't know who Michael Feathers is, go buy/read his book "Working Effectively with Legacy Code". He is a person worth knowing about if you are a professional software engineer/developer). There are other (anti)patterns that cause this issue to crop up, but this is by far the most common one I've stumbled across. "Iceberg" classes have one public method, and the rest are private (which is why it's tempting to test the private methods). It's called an "Iceberg" class because there is usually a lone public method poking up, but the rest of the functionality is hidden underwater in the form of private methods. It might look something like this:

例如,您可能希望通过在字符串上连续调用GetNextToken()来测试它,并查看它是否返回预期的结果。这样的函数确实需要进行测试:该行为不是微不足道的,特别是如果您的标记规则很复杂的话。让我们假设它并没有那么复杂,我们只是想要用空格分隔的标记。所以你写了一个测试,它可能看起来像这样(一些语言不可知的伪代码,希望想法是清楚的):

TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    re = RuleEvaluator(input_string);

    ASSERT re.GetNextToken() IS "1";
    ASSERT re.GetNextToken() IS "2";
    ASSERT re.GetNextToken() IS "test";
    ASSERT re.GetNextToken() IS "bar";
    ASSERT re.HasMoreTokens() IS FALSE;
}

Well, that actually looks pretty nice. We'd want to make sure we maintain this behavior as we make changes. But GetNextToken() is a private function! So we can't test it like this, because it wont even compile (assuming we are using some language that actually enforces public/private, unlike some scripting languages like Python). But what about changing the RuleEvaluator class to follow the Single Responsibility Principle (Single Responsibility Principle)? For instance, we seem to have a parser, tokenizer, and evaluator jammed into one class. Wouldn't it be better to just separate those responsibilities? On top of that, if you create a Tokenizer class, then it's public methods would be HasMoreTokens() and GetNextTokens(). The RuleEvaluator class could have a Tokenizer object as a member. Now, we can keep the same test as above, except we are testing the Tokenizer class instead of the RuleEvaluator class.

下面是它在UML中的样子:

注意,这种新设计增加了模块化,因此您可能会在系统的其他部分重用这些类(在此之前,私有方法根据定义是不可重用的)。这是分解RuleEvaluator的主要优势,同时增加了可理解性/局部性。

这个测试看起来非常相似,除了这次它实际上是编译的,因为GetNextToken()方法现在在Tokenizer类上是公共的:

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS FALSE;
}

通过公共接口测试私有组件,避免重复测试

Even if you don't think you can break your problem down into fewer modular components (which you can 95% of the time if you just try to do it), you can simply test the private functions through a public interface. Many times private members aren't worth testing because they will be tested through the public interface. A lot of times what I see is tests that look very similar, but test two different functions/methods. What ends up happening is that when requirements change (and they always do), you now have 2 broken tests instead of 1. And if you really tested all your private methods, you might have more like 10 broken tests instead of 1. In short, testing private functions (by using FRIEND_TEST or making them public or using reflection) that could otherwise be tested through a public interface can cause test duplication. You really don't want this, because nothing hurts more than your test suite slowing you down. It's supposed to decrease development time and decrease maintenance costs! If you test private methods that are otherwise tested through a public interface, the test suite may very well do the opposite, and actively increase maintenance costs and increase development time. When you make a private function public, or if you use something like FRIEND_TEST and/or reflection, you'll usually end up regretting it in the long run.

考虑Tokenizer类的以下可能实现:

假设SplitUpByDelimiter()负责返回一个数组,使数组中的每个元素都是一个令牌。此外,假设GetNextToken()只是这个向量上的迭代器。所以你的公开考试可能是这样的:

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS false;
}

让我们假设我们有迈克尔·费瑟所说的“摸索工具”。这个工具可以让你触摸别人的隐私部位。一个例子是googletest中的FRIEND_TEST,如果语言支持则为reflection。

TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);
    result_array = tokenizer.SplitUpByDelimiter(" ");

    ASSERT result.size() IS 4;
    ASSERT result[0] IS "1";
    ASSERT result[1] IS "2";
    ASSERT result[2] IS "test";
    ASSERT result[3] IS "bar";
}

好吧,现在让我们假设需求发生了变化,标记化变得更加复杂。您认为一个简单的字符串分隔符是不够的,需要一个delimiter类来处理这项工作。当然,您希望有一个测试失败,但是当您测试私有函数时,这种痛苦会增加。

什么时候测试私有方法是合适的?

在软件中没有“一刀切”。有时“打破规则”是可以的(实际上是理想的)。我强烈建议,如果可以的话,不要测试私有功能。主要有两种情况,我认为这是可以接受的:

I've worked extensively with legacy systems (which is why I'm such a big Michael Feathers fan), and I can safely say that sometimes it is simply safest to just test the private functionality. It can be especially helpful for getting "characterization tests" into the baseline. You're in a rush, and have to do the fastest thing possible for here and now. In the long run, you don't want to test private methods. But I will say that it usually takes some time to refactor to address design issues. And sometimes you have to ship in a week. That's okay: do the quick and dirty and test the private methods using a groping tool if that's what you think is the fastest and most reliable way to get the job done. But understand that what you did was suboptimal in the long run, and please consider coming back to it (or, if it was forgotten about but you see it later, fix it).

也许在其他情况下,这是可以接受的。如果你认为这是可以的,并且你有一个很好的理由,那么就去做。没人阻止你。只是要注意潜在的成本。

TDD的借口

As an aside, I really don't like people using TDD as an excuse for testing private methods. I practice TDD, and I do not think TDD forces you to do this. You can write your test (for your public interface) first, and then write code to satisfy that interface. Sometimes I write a test for a public interface, and I'll satisfy it by writing one or two smaller private methods as well (but I don't test the private methods directly, but I know they work or my public test would be failing). If I need to test edge cases of that private method, I'll write a whole bunch of tests that will hit them through my public interface. If you can't figure out how to hit the edge cases, this is a strong sign you need to refactor into small components each with their own public methods. It's a sign your private functions are doing too much, and outside the scope of the class.

Also, sometimes I find I write a test that is too big of a bite to chew at the moment, and so I think "eh I'll come back to that test later when I have more of an API to work with" (I'll comment it out and keep it in the back of my mind). This is where a lot of devs I've met will then start writing tests for their private functionality, using TDD as the scapegoat. They say "oh, well I need some other test, but in order to write that test, I'll need these private methods. Therefore, since I can't write any production code without writing a test, I need to write a test for a private method." But what they really need to be doing is refactoring into smaller and reusable components instead of adding/testing a bunch of private methods to their current class.

注意:

不久前,我回答了一个关于使用GoogleTest测试私有方法的类似问题。在这里,我修改了这个答案,使之更加语言不可知。

附注:以下是Michael Feathers关于冰山课程和探索工具的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU


我从来不理解单元测试的概念,但现在我知道它的目标是什么了。

单元测试不是一个完整的测试。所以,它并不是QA和手动测试的替代品。在这方面,TDD的概念是错误的,因为你不能测试所有的东西,包括私有方法,也包括使用资源的方法(特别是我们无法控制的资源)。TDD的所有质量都是无法实现的。

单元测试更像是一个枢轴测试,你标记一些任意的枢轴,枢轴的结果应该保持不变。


它不仅涉及公共或私有方法或函数,还涉及实现细节。私有函数只是实现细节的一个方面。

毕竟,单元测试是一种白盒测试方法。例如,无论谁使用覆盖率分析来识别到目前为止在测试中被忽略的代码部分,都将进入实现细节。

A)是的,你应该测试实现细节:

考虑一个排序函数,如果有多达10个元素,则出于性能考虑使用BubbleSort的私有实现,如果有超过10个元素,则使用不同排序方法(例如堆排序)的私有实现。公共API是排序函数的API。但是,您的测试套件更好地利用了实际上使用了两种排序算法的知识。

在本例中,当然可以在公共API上执行测试。然而,这需要有大量的测试用例来执行具有10个以上元素的排序函数,以便对堆排序算法进行充分的测试。这种测试用例的单独存在表明测试套件连接到功能的实现细节。

如果排序函数的实现细节发生了变化,可能是两种排序算法之间的限制发生了变化,或者堆排序被归并排序取代了,或者其他:现有的测试将继续工作。然而,它们的价值是值得怀疑的,它们可能需要重新工作,以更好地测试更改后的排序函数。换句话说,尽管测试是在公共API上进行的,但仍然需要进行维护工作。

B)如何测试实现细节

许多人认为不应该测试私有函数或实现细节的一个原因是,实现细节更有可能改变。这种更高的更改可能性至少是将实现细节隐藏在接口后面的原因之一。

现在,假设接口背后的实现包含更大的私有部分,可以选择在内部接口上进行单独的测试。有些人认为,这些部分不应该在私人的时候进行测试,它们应该变成公共的东西。一旦公开,对代码进行单元测试就可以了。

这很有趣:虽然接口是内部的,但它很可能会改变,这是一个实现细节。使用相同的接口,将其公开可以进行一些神奇的转换,即将其转换为不太可能更改的接口。显然,这种论证有一些缺陷。

但是,这背后仍然有一些事实:在测试实现细节时,特别是使用内部接口时,应该尽量使用可能保持稳定的接口。然而,某些接口是否可能是稳定的,不能简单地根据它是公共的还是私有的来确定。在我工作过一段时间的项目中,公共接口也经常发生变化,而许多私有接口多年未动。

不过,“先从正门走”是一个很好的经验法则(见http://xunitpatterns.com/Principles%20of%20Test%20Automation.html)。但请记住,这是“前门优先”,而不是“只有前门”。

C)总结

还要测试实现细节。更喜欢在稳定接口(公共或私有)上进行测试。如果实现细节发生变化,也需要修改对公共API的测试。把私人的东西变成公共的东西并不能神奇地改变它的稳定性。


是的,您应该在任何可能的地方测试私有方法。为什么?避免不必要的测试用例状态空间爆炸,最终只是在相同的输入上隐式地重复测试相同的私有函数。让我们用一个例子来解释为什么。

考虑一下下面略显做作的例子。假设我们想公开一个函数,该函数接受3个整数,当且仅当这3个整数都是素数时返回true。我们可以这样实现它:

public bool allPrime(int a, int b, int c)
{
  return andAll(isPrime(a), isPrime(b), isPrime(c))
}

private bool andAll(bool... boolArray)
{
  foreach (bool b in boolArray)
  {
    if(b == false) return false;
  }
  return true;
}

private bool isPrime(int x){
  //Implementation to go here. Sorry if you were expecting a prime sieve.
}

现在,如果我们采取严格的方法,只测试公共函数,我们只允许测试allPrime,而不允许测试isPrime或andAll。

作为测试人员,我们可能对每个参数的五种可能性感兴趣:< 0,= 0,= 1,质数> 1,而不是质数> 1。但为了彻底,我们还必须看看每个参数的组合是如何发挥作用的。根据我们的直觉,我们需要5*5*5 = 125个测试用例来彻底测试这个函数。

On the other hand, if we were allowed to test the private functions, we could cover as much ground with fewer test cases. We'd need only 5 test cases to test isPrime to the same level as our previous intuition. And by the small scope hypothesis proposed by Daniel Jackson, we'd only need to test the andAll function up to a small length e.g. 3 or 4. Which would be at most 16 more tests. So 21 tests in total. Instead of 125. Of course, we probably would want to run a few tests on allPrime, but we wouldn't feel so obliged to cover exhaustively all 125 combinations of input scenarios we said we cared about. Just a few happy paths.

当然,这是一个虚构的例子,但为了清晰地演示,这是必要的。这种模式可以扩展到真实的软件中。私有函数通常是最低级别的构建块,因此经常组合在一起以产生更高级别的逻辑。也就是说,在较高的层次上,由于不同的组合,我们对较低层次的东西有更多的重复。


你也可以让你的方法包私有,即默认,你应该能够单元测试它,除非它被要求是私有的。


对于从测试中调用什么api,公共和私有并不是一个有用的区分,方法和类也不是。大多数可测试单元在一个上下文中是可见的,但在其他上下文中是隐藏的。

重要的是覆盖范围和成本。您需要最小化成本,同时实现项目的覆盖目标(行、分支、路径、块、方法、类、等价类、用例……)不管团队怎么决定)。

因此,使用工具来确保覆盖率,并设计您的测试以减少成本(短期和长期)。

不要让测试变得过于昂贵。 如果只测试公共入口是最便宜的,那就这样做。 如果测试私有方法成本最低,那就这么做。

随着您的经验越来越丰富,您将能够更好地预测何时值得重构以避免测试维护的长期成本。