我已经阅读了有关如何测试私有方法的这篇帖子。通常情况下,我不会测试私有方法,因为我一直认为仅测试从对象外部调用的公共方法会更快。您会测试私有方法吗?我应该始终测试它们吗?
我已经阅读了有关如何测试私有方法的这篇帖子。通常情况下,我不会测试私有方法,因为我一直认为仅测试从对象外部调用的公共方法会更快。您会测试私有方法吗?我应该始终测试它们吗?
我不对私有方法进行单元测试。私有方法是一个应该对类的使用者隐藏的实现细节。测试私有方法会破坏封装性。
如果我发现这个私有方法很庞大、复杂或者非常重要以至于需要自己的测试,我会把它放到另一个类中并在那里将其公开(方法对象)。然后,我可以轻松地测试之前被私有化但现在已经存在于自己类中的公共方法。
测试的目的是什么?
迄今为止,大多数回答都表示私有方法是实现细节,只要公共接口经过充分测试并正常工作,就不会(或者至少不应该)有影响。如果您进行测试的唯一目的是确保公共接口工作,那么这是完全正确的。
就我个人而言,我的主要代码测试用途是确保未来的代码更改不会导致问题,并且在出现问题时帮助我的调试工作。我发现像测试公共接口一样彻底地测试私有方法(如果不是更加彻底!)可以进一步实现这个目的。
考虑一下:您有一个公共方法A,它调用私有方法B。A和B都使用方法C。C被更改(也许是您自己,也许是供应商),导致A开始失败其测试。即使它是私有的,拥有B的测试是否有用,这样您就知道问题是在A对C的使用、B对C的使用还是两者都存在问题。
测试私有方法还可以在公共接口的测试覆盖率不完整的情况下增加价值。虽然我们通常希望避免这种情况,但单元测试的效率取决于测试是否能够发现错误以及这些测试的开发和维护成本。在某些情况下,100%的测试覆盖率的好处可能被认为不足以证明这些测试的成本,从而产生公共接口测试覆盖的空白。在这种情况下,对私有方法进行有针对性的测试可以是代码库中非常有效的补充。
testDoSomething()
或testDoSomethingPrivate()
中的任意一个。这会使测试变得不太有价值。这里有更多关于测试私有方法的原因:http://stackoverflow.com/questions/34571/how-to-test-a-class-that-has-private-methods-fields-or-inner-classes#comment9624053_34571。 - Pacerier我倾向于遵循Dave Thomas和Andy Hunt在他们的书Pragmatic Unit Testing中的建议:
一般情况下,出于测试目的,您不想破坏任何封装(正如妈妈曾经说过的“不要暴露你的私人部位!”)。大多数时候,您应该能够通过执行其公共方法来测试类。如果有重要功能隐藏在私有或受保护的访问后面,这可能是另一个正在努力挣扎的类的警告标志。
但是有时我会忍不住测试私有方法,因为它给了我一种安心的感觉,即我正在构建一个完全健壮的程序。
我不喜欢测试私有功能的原因有几个,主要如下:
我将使用具体的例子来解释每个原因。其实2)和3)有些密切相关,所以他们的例子是类似的,尽管我认为它们是你不应该测试私有方法的分别原因。
当然,有时测试私有方法是适当的,但重要的是要意识到上述缺点。稍后我会详细介绍。
我还会在最后详细介绍为什么TDD不是测试私有方法的有效借口。
我看到最常见的(反)模式之一是Michael Feathers所称的“冰山”类(如果你不知道Michael Feathers是谁,去购买/阅读他的书“遗留代码有效工作”。如果你是专业软件工程师/开发人员,他是值得了解的人)。还有其他(反)模式会导致这个问题出现,但这是我遇到的最常见的一个。 “冰山”类只有一个公共方法,其余都是私有的(这就是为什么测试私有方法很诱人的原因)。它被称为“冰山”类,因为通常有一个孤立的公共方法,但其余功能以私有方法的形式隐藏在水下。 它可能看起来像这样:
例如,您可能想通过连续调用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;
}
好的,这看起来非常不错。我们需要确保在进行更改时保持这种行为。但是GetNextToken()
是一个私有函数!所以我们不能像这样测试它,因为它甚至不会编译(假设我们使用一些实际执行公共/私有的语言,而不是像Python这样的某些脚本语言)。但是,如果将RuleEvaluator
类更改为遵循单一职责原则(Single Responsibility Principle),怎么样呢?例如,我们似乎将解析器、标记化器和评估器挤入了一个类中。仅将这些责任分离开来是否更好?此外,如果创建一个Tokenizer
类,则其公共方法将为HasMoreTokens()
和GetNextTokens()
。 RuleEvaluator
类可以将Tokenizer
对象作为成员。现在,我们可以保持与上面相同的测试,只是我们正在测试Tokenizer
类而不是RuleEvaluator
类。
以下是UML中的示例:
请注意,这种新设计增加了模块化,因此您可以在系统的其他部分重新使用这些类(在此之前不行,私有方法根据定义不可重用)。这是将RuleEvaluator拆分的主要优点,以及增加了可理解性/局部性。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;
}
假设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;
}
让我们假设我们有一个被 Michael Feather 称为“ groping tool”的工具。这是一种让您触摸他人私处的工具。例如,googletest 的 FRIEND_TEST 或语言支持的反射机制。
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
类来处理这个任务。自然地,您会期望一个测试失败,但当您测试私有函数时,这种痛苦会增加。
在软件中没有“一刀切”的规定。有时候打破规则是可以的(实际上是理想的)。我强烈建议您尽可能不要测试私有功能。以下是我认为可以测试私有功能的两种情况:
我曾经广泛地使用遗留系统(这也是我为什么是Michael Feathers的粉丝),我可以肯定地说,有时候只测试私有功能是最安全的做法。它对于将"特性测试"纳入基线尤其有帮助。
您很匆忙,必须尽快完成任务。从长远来看,您不想测试私有方法。但是我会说,要解决设计问题通常需要一些时间进行重构。有时候你必须在一周内发货。没关系: 如果您认为使用 groping 工具测试私有方法是最快和最可靠的方法来完成工作,请执行快速而肮脏的操作并进行测试。但请理解,您所做的事情从长远来看是次优的,请考虑回过头来(或者如果它被遗忘了,但您后来看到了它,请修复它)。
可能还有其他情况是可以的。如果您认为这样做没问题,并且有充分的理由,请去做吧。没有人会阻止你。只需注意潜在的成本。
顺带一提,我真的不喜欢有人用TDD作为测试私有方法的借口。我实践TDD,我不认为TDD会强制你这样做。您可以先为公共接口编写测试,然后编写代码以满足该接口。有时我会为公共接口编写一个测试,并通过编写一个或两个较小的私有方法来满足它(但我不直接测试私有方法,但我知道它们有效,否则我的公共测试将失败)。如果我需要测试该私有方法的边缘情况,我将编写大量测试,通过我的公共接口来测试它们。如果您无法找出如何触发边缘情况,则这是您需要将其重构为具有自己的公共方法的小组件的强烈信号。这表明您的私有函数正在做太多事情,超出了类的范围。
有时候我会写一个测试,但发现它太大了,难以一口吃下,所以我会想:“嗯,等我有更多的 API 可以使用时,我再回来测试这个”(我会注释掉并记在脑后)。这就是我遇到过的许多开发人员开始为他们的私有功能编写测试,并以 TDD 为替罪羊的地方。他们说:“哦,我需要另一个测试,但为了编写那个测试,我需要这些私有方法。因此,既然我不能编写任何生产代码而不编写测试,我需要为私有方法编写测试。” 但他们真正需要做的是将其重构为更小和可重用的组件,而不是向当前类添加/测试许多私有方法。_
命名所谓的“私有”成员,它就会发出信号:“嘿,这是‘私有’的。你可以使用它,但要完全透明,它不是为了重复使用而设计的,只有在你真正知道自己在做什么时才能使用它。” 你可以在任何语言中采取同样的方法:将这些成员公开,但用前导下划线 _
标记它们。或者也许这些函数确实应该是私有的,并且只通过公共接口进行测试(详见答案)。这是具体情况具体分析,没有通用规则。 - Matt Messersmith每个函数中不超过10个圈复杂度。
现在该策略的执行会导致我的很多非常大的公共函数被分解成更多聚焦、命名更好的私有函数。
公共函数仍然存在(当然),但本质上被减少为调用所有那些私有的 "子函数"。
这其实很酷,因为调用栈现在更容易阅读了(与一个大函数内的 bug 不同,我现在有一个帮助我理解“如何到达那里”的先前函数名称的子子函数中的 bug)。
然而,现在似乎更容易直接对这些私有函数进行单元测试,并将大型公共函数的测试留给某种“集成”测试来处理场景。
只是我的看法。
我确实测试私有函数,因为虽然它们被公共方法测试过了,但在测试驱动开发(TDD)中,测试应用程序的最小部分是很好的实践。但是,在测试单元类中无法访问私有函数。以下是我们测试私有方法的方法。
为什么要使用私有方法?
私有函数主要存在于我们的类中,因为我们希望在公共方法中创建易读的代码。 我们不希望该类的用户直接调用这些方法,而是通过我们的公共方法间接调用它们。此外,我们也不希望在扩展类时更改它们的行为(在受保护的情况下),因此设置为私有。
在编码时,我们采用测试驱动设计(TDD)。这意味着有时我们会遇到需要测试但是是私有功能的情况。PhpUnit中无法测试私有函数,因为我们不能在测试类中访问它们(它们是私有的)。
我们认为有三种解决方案:
1. 通过公共方法测试私有函数
优点
缺点
2. 如果私有函数如此重要,则创建一个新的独立类来处理它可能更好
优点
缺点
3. 更改访问修饰符为(final)protected
优点
缺点
示例
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来测试我们之前的私有函数。
我认为最好只测试对象的公共接口。从外部世界的角度来看,只有公共接口的行为才是重要的,这也是你应该针对其进行单元测试的原因。
一旦你已经编写了一些可靠的对象单元测试,你不希望因为接口后面的实现更改而回去改变那些测试。在这种情况下,你破坏了单元测试的一致性。
如果私有方法被良好定义(即具有可测试的函数且不打算随时间改变),那么是的。我会在有意义的情况下测试一切可测试的内容。
例如,加密库可能会使用一个私有方法隐藏其进行块加密并每次只加密8个字节的事实。我会为此编写单元测试 - 尽管它被隐藏,但它不打算改变,如果它确实出现了问题(例如由于未来性能提升引起),那么我想知道是私有函数出错了,而不仅仅是公共函数出错了。
这样可以加快后续调试速度。
-Adam
如果您正在进行测试驱动开发 (TDD),您将会对私有方法进行测试。