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


当前回答

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

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

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

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

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

其他回答

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

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

出于几个原因,我不喜欢测试私有功能。它们如下(这些是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

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

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

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

它加快了以后的调试。

亚当