我已经阅读了这篇关于如何测试私有方法的文章。我通常不测试它们,因为我总是认为只测试将从对象外部调用的公共方法更快。你测试私有方法吗?我应该一直测试它们吗?
当前回答
其中一个要点是
如果我们测试以确保逻辑的正确性,并且私有方法携带逻辑,那么我们应该测试它。不是吗?我们为什么要跳过这个呢?
基于方法的可见性编写测试是完全无关的想法。
相反
另一方面,在原始类之外调用私有方法是一个主要问题。在一些模拟工具中,模拟私有方法也有局限性。(例:5)
虽然有一些工具如Power Mock支持这一点,但这是一个危险的操作。原因是它需要破解JVM来实现这一点。
一种可以解决的方法是(如果您想为私有方法编写测试用例)
将这些私有方法声明为受保护的。但在某些情况下可能不方便。
其他回答
出于几个原因,我不喜欢测试私有功能。它们如下(这些是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
如果我发现私有方法非常庞大、复杂或重要到需要自己的测试,我就把它放在另一个类中,并在那里将它设为公共(方法对象)。然后,我可以轻松地测试以前私有但现在是公共的方法,它现在存在于自己的类中。
它不仅涉及公共或私有方法或函数,还涉及实现细节。私有函数只是实现细节的一个方面。
毕竟,单元测试是一种白盒测试方法。例如,无论谁使用覆盖率分析来识别到目前为止在测试中被忽略的代码部分,都将进入实现细节。
A)是的,你应该测试实现细节:
考虑一个排序函数,如果有多达10个元素,则出于性能考虑使用BubbleSort的私有实现,如果有超过10个元素,则使用不同排序方法(例如堆排序)的私有实现。公共API是排序函数的API。但是,您的测试套件更好地利用了实际上使用了两种排序算法的知识。
在本例中,当然可以在公共API上执行测试。然而,这需要有大量的测试用例来执行具有10个以上元素的排序函数,以便对堆排序算法进行充分的测试。这种测试用例的单独存在表明测试套件连接到功能的实现细节。
如果排序函数的实现细节发生了变化,可能是两种排序算法之间的限制发生了变化,或者堆排序被归并排序取代了,或者其他:现有的测试将继续工作。然而,它们的价值是值得怀疑的,它们可能需要重新工作,以更好地测试更改后的排序函数。换句话说,尽管测试是在公共API上进行的,但仍然需要进行维护工作。
B)如何测试实现细节
许多人认为不应该测试私有函数或实现细节的一个原因是,实现细节更有可能改变。这种更高的更改可能性至少是将实现细节隐藏在接口后面的原因之一。
现在,假设接口背后的实现包含更大的私有部分,可以选择在内部接口上进行单独的测试。有些人认为,这些部分不应该在私人的时候进行测试,它们应该变成公共的东西。一旦公开,对代码进行单元测试就可以了。
这很有趣:虽然接口是内部的,但它很可能会改变,这是一个实现细节。使用相同的接口,将其公开可以进行一些神奇的转换,即将其转换为不太可能更改的接口。显然,这种论证有一些缺陷。
但是,这背后仍然有一些事实:在测试实现细节时,特别是使用内部接口时,应该尽量使用可能保持稳定的接口。然而,某些接口是否可能是稳定的,不能简单地根据它是公共的还是私有的来确定。在我工作过一段时间的项目中,公共接口也经常发生变化,而许多私有接口多年未动。
不过,“先从正门走”是一个很好的经验法则(见http://xunitpatterns.com/Principles%20of%20Test%20Automation.html)。但请记住,这是“前门优先”,而不是“只有前门”。
C)总结
还要测试实现细节。更喜欢在稳定接口(公共或私有)上进行测试。如果实现细节发生变化,也需要修改对公共API的测试。把私人的东西变成公共的东西并不能神奇地改变它的稳定性。
其中一个要点是
如果我们测试以确保逻辑的正确性,并且私有方法携带逻辑,那么我们应该测试它。不是吗?我们为什么要跳过这个呢?
基于方法的可见性编写测试是完全无关的想法。
相反
另一方面,在原始类之外调用私有方法是一个主要问题。在一些模拟工具中,模拟私有方法也有局限性。(例:5)
虽然有一些工具如Power Mock支持这一点,但这是一个危险的操作。原因是它需要破解JVM来实现这一点。
一种可以解决的方法是(如果您想为私有方法编写测试用例)
将这些私有方法声明为受保护的。但在某些情况下可能不方便。
这显然与语言有关。在过去的c++中,我将测试类声明为友类。不幸的是,这需要您的生产代码了解测试类。
推荐文章
- python中的assertEquals和assertEqual
- 如何使用“测试”包打印Go测试?
- 设计模式:工厂vs工厂方法vs抽象工厂
- 如何在IntelliJ中为整个项目配置“缩短命令行”方法
- 为什么处理排序数组比未排序数组慢?
- toBe(true) vs toBeTruthy() vs toBeTrue()
- 构造函数何时抛出异常是正确的?
- 使用Mockito测试抽象类
- 什么是“一级”对象?
- 什么时候应该使用Debug.Assert()?
- 为什么浮点数不准确?
- 使用Moq模拟单元测试的异步方法
- 你的项目没有引用“. net framework,Version=v4.6.2”框架。在“TargetFrameworks”中添加对“.NETFramework,Version=v4.6.2”的引用
- 使用Moq模拟扩展方法
- 从控制台停止一个Android应用程序