我已经阅读了这篇关于如何测试私有方法的文章。我通常不测试它们,因为我总是认为只测试将从对象外部调用的公共方法更快。你测试私有方法吗?我应该一直测试它们吗?
当前回答
我不对私有方法进行单元测试。私有方法是应该对类的用户隐藏的实现细节。测试私有方法会破坏封装。
如果我发现私有方法非常庞大、复杂或重要到需要自己的测试,我就把它放在另一个类中,并在那里将它设为公共(方法对象)。然后,我可以轻松地测试以前是私有但现在是公共的方法,该方法现在存在于它自己的类中。
其他回答
如果您不测试您的私有方法,您怎么知道它们不会崩溃呢?
出于几个原因,我不喜欢测试私有功能。它们如下(这些是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
“我应该测试私有方法吗?”的答案是“有时.......”。通常情况下,您应该针对类的接口进行测试。
其中一个原因是您不需要对一个特性进行双重覆盖。 另一个原因是,如果您更改了私有方法,则必须为它们更新每个测试,即使对象的接口根本没有更改。
这里有一个例子:
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
这不应该由外部用户来运行,但是您的封装类可能太笨重了,无法一遍又一遍地运行这么多逻辑。在这种情况下,您可能更愿意将其提取到一个单独的类中,为该类提供一个接口并对其进行测试。
最后,假设你的主对象非常重,方法非常小你需要确保输出是正确的。你会想,“我必须测试这个私有方法!”也许你可以通过传入一些繁重的工作作为初始化参数使你的对象更轻?然后你可以放一些更轻的东西进去测试。
是的,您应该在任何可能的地方测试私有方法。为什么?避免不必要的测试用例状态空间爆炸,最终只是在相同的输入上隐式地重复测试相同的私有函数。让我们用一个例子来解释为什么。
考虑一下下面略显做作的例子。假设我们想公开一个函数,该函数接受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.
当然,这是一个虚构的例子,但为了清晰地演示,这是必要的。这种模式可以扩展到真实的软件中。私有函数通常是最低级别的构建块,因此经常组合在一起以产生更高级别的逻辑。也就是说,在较高的层次上,由于不同的组合,我们对较低层次的东西有更多的重复。
是的,我确实测试私有函数,因为尽管它们是由你的公共方法测试的,但在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来测试之前的私有函数。