如何使用 JUnit 测试具有内部私有方法、字段或嵌套类的类?
仅仅为了能够运行测试而更改方法的访问修饰符似乎很糟糕。
更新:
大约 10 年后,也许测试私有方法或任何不可访问成员的最佳方法是通过
@Jailbreak
Manifold框架。@Jailbreak Foo foo = new Foo(); // Direct, *type-safe* access to *all* foo's members foo.privateMethod(x, y, z); foo.privateField = value;
这样,您的代码将保持类型安全和可读性。没有设计妥协,没有为了测试而过度曝光的方法和领域。
如果您有一些遗留的Java应用程序,并且不允许更改方法的可见性,那么测试私有方法的最佳方法是使用反射。
在内部,我们使用助手来获取/设置private
和private static
变量以及调用private
和private static
方法。以下模式将让您几乎可以做任何与私有方法和字段相关的事情。当然,你private static final
不能通过反射来改变变量。
Method method = TargetClass.getDeclaredMethod(methodName, argClasses);
method.setAccessible(true);
return method.invoke(targetObject, argObjects);
对于字段:
Field field = TargetClass.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
注意事项:
1.TargetClass.getDeclaredMethod(methodName, argClasses)
让您查看private
方法。同样的事情适用于getDeclaredField
.
2.setAccessible(true)
需要和私人一起玩。
测试私有方法的最佳方法是通过另一种公共方法。如果无法做到这一点,则以下条件之一为真:
当我在一个类中有足够复杂的私有方法时,我觉得有必要直接测试私有方法,那就是代码味道:我的类太复杂了。
我解决此类问题的常用方法是梳理出一个包含有趣部分的新类。通常,这个方法和与之交互的字段,也许还有另外一两个方法可以被提取到一个新的类中。
新类将这些方法公开为“公共”,因此它们可用于单元测试。新旧类现在都比原来的类更简单,这对我来说很棒(我需要保持简单,否则我会迷路!)。
请注意,我并不是建议人们在不使用大脑的情况下创建课程!这里的重点是使用单元测试的力量来帮助您找到好的新类。
过去我曾使用反射为 Java 执行此操作,在我看来这是一个很大的错误。
严格来说,您不应该编写直接测试私有方法的单元测试。您应该测试的是该类与其他对象的公共契约;您永远不应该直接测试对象的内部结构。如果另一个开发人员想要对类进行小的内部更改,这不会影响类公共合同,那么他/她必须修改基于反射的测试以确保它有效。如果您在整个项目中反复这样做,那么单元测试将不再是衡量代码健康状况的有用指标,而是开始成为开发的障碍和开发团队的烦恼。
我建议改为使用 Cobertura 之类的代码覆盖率工具,以确保您编写的单元测试在私有方法中提供良好的代码覆盖率。这样,您可以间接测试私有方法在做什么,并保持更高水平的敏捷性。
从这篇文章:Testing Private Methods with JUnit and SuiteRunner (Bill Venners),你基本上有 4 个选项:
- 不要测试私有方法。
- 授予方法包访问权限。
- 使用嵌套测试类。
- 使用反射。
通常,单元测试旨在练习类或单元的公共接口。因此,私有方法是您不希望显式测试的实现细节。
只有两个我想测试私有方法的例子:
SecurityManager
而没有配置为防止这种情况发生)。我理解只测试“合同”的想法。但我看不出有人可以提倡实际上不测试代码 - 你的里程可能会有所不同。
所以我的权衡涉及使用反射使 JUnit 复杂化,而不是损害我的安全性和 SDK。
私有方法由公共方法调用,因此公共方法的输入还应测试由这些公共方法调用的私有方法。当公共方法失败时,那可能是私有方法的失败。
在Spring Framework中,您可以使用此方法测试私有方法:
ReflectionTestUtils.invokeMethod()
例如:
ReflectionTestUtils.invokeMethod(TestClazz, "createTest", "input data");
我使用的另一种方法是将私有方法更改为封装私有或受保护,然后使用Google Guava 库的@VisibleForTesting注释对其进行补充。
这将告诉任何使用此方法的人要小心,即使在包中也不要直接访问它。此外,测试类不必物理上在同一个包中,而是在测试文件夹下的同一个包中。
例如,如果要测试的方法在 insrc/main/java/mypackage/MyClass.java
那么您的测试调用应该放在src/test/java/mypackage/MyClassTest.java
. 这样,您就可以访问测试类中的测试方法。
要测试具有大而古怪的类的遗留代码,能够测试我现在正在编写的一个私有(或公共)方法通常非常有帮助。
我使用junitx.util.PrivateAccessor -package for Java 。许多有用的单行代码用于访问私有方法和私有字段。
import junitx.util.PrivateAccessor;
PrivateAccessor.setField(myObjectReference, "myCrucialButHardToReachPrivateField", myNewValue);
PrivateAccessor.invoke(myObjectReference, "privateMethodName", java.lang.Class[] parameterTypes, java.lang.Object[] args);
在尝试使用 Java 反射的Cem Catikkas解决方案后,我不得不说他的解决方案比我在这里描述的更优雅。但是,如果您正在寻找使用反射的替代方法,并且可以访问您正在测试的源,这仍然是一个选项。
测试类的私有方法可能有好处,特别是在测试驱动开发中,您希望在编写任何代码之前设计小型测试。
创建一个可以访问私有成员和方法的测试可以测试难以专门针对仅访问公共方法的代码区域。如果公共方法涉及多个步骤,则它可以包含多个私有方法,然后可以单独对其进行测试。
好处:
缺点:
但是,如果连续测试需要这种方法,这可能是一个信号,应该提取私有方法,可以用传统的公共方式进行测试。
这是一个令人费解的例子,说明它是如何工作的:
// Import statements and package declarations
public class ClassToTest
{
private int decrement(int toDecrement) {
toDecrement--;
return toDecrement;
}
// Constructor and the rest of the class
public static class StaticInnerTest extends TestCase
{
public StaticInnerTest(){
super();
}
public void testDecrement(){
int number = 10;
ClassToTest toTest= new ClassToTest();
int decremented = toTest.decrement(number);
assertEquals(9, decremented);
}
public static void main(String[] args) {
junit.textui.TestRunner.run(StaticInnerTest.class);
}
}
}
内部类将被编译为ClassToTest$StaticInnerTest
.
正如其他人所说......不要直接测试私有方法。这里有一些想法:
在单元测试中运行代码覆盖率。如果您发现方法未经过全面测试,请添加到测试中以提高覆盖率。以 100% 的代码覆盖率为目标,但要意识到你可能不会得到它。
私有方法被公共方法使用。否则,它们就是死代码。这就是为什么你测试公共方法,断言公共方法的预期结果,从而断言它使用的私有方法。
在对公共方法运行单元测试之前,应该通过调试来测试私有方法。
也可以使用测试驱动开发来调试它们,调试单元测试直到满足所有断言。
我个人认为使用 TDD 创建类会更好;创建公共方法存根,然后使用预先定义的所有断言生成单元测试,因此在编码之前确定方法的预期结果。这样,您就不会走上错误的道路,使单元测试断言适合结果。当您的所有单元测试都通过时,您的类就会变得健壮并满足要求。
如果使用 Spring,ReflectionTestUtils提供了一些方便的工具,可以帮助您轻松完成任务。例如,要在私有成员上设置模拟而不被迫添加不受欢迎的公共设置器:
ReflectionTestUtils.setField(theClass, "theUnsettableField", theMockObject);
如果您正在尝试测试您不愿意或无法更改的现有代码,那么反射是一个不错的选择。
如果该类的设计仍然灵活,并且您有一个复杂的私有方法想要单独测试,我建议您将其拉到一个单独的类中并单独测试该类。这不必更改原始类的公共接口;它可以在内部创建助手类的实例并调用助手方法。
如果您想测试来自辅助方法的困难错误条件,您可以更进一步。从帮助类中提取一个接口,在原类中添加一个公共的getter和setter来注入帮助类(通过它的接口使用),然后在原类中注入一个mock版本的帮助类来测试原类如何响应助手的异常。如果您想测试原始类而不测试辅助类,这种方法也很有帮助。
测试私有方法会破坏类的封装,因为每次更改内部实现时都会破坏客户端代码(在本例中是测试)。
所以不要测试私有方法。
如果您想测试无法更改代码的遗留应用程序的私有方法,Java 的一个选项是jMockit,它允许您为对象创建模拟,即使它们是类私有的。
JUnit.org FAQ 页面的答案:
但如果你必须...
如果您使用的是 JDK 1.3 或更高版本,则可以借助PrivilegedAccessor使用反射来颠覆访问控制机制。有关如何使用它的详细信息,请阅读这篇文章。
如果您使用 JDK 1.6 或更高版本并使用 @Test 注释测试,则可以使用Dp4j在测试方法中注入反射。有关如何使用它的详细信息,请参阅此测试脚本。
我倾向于不测试私有方法。那里有疯狂。就个人而言,我认为你应该只测试你公开的接口(包括受保护的和内部的方法)。
如果您使用的是 JUnit,请查看junit-addons。它能够忽略 Java 安全模型并访问私有方法和属性。
我建议你稍微重构一下你的代码。当您不得不开始考虑使用反射或其他类型的东西来测试您的代码时,您的代码就会出现问题。
你提到了不同类型的问题。让我们从私有字段开始。如果是私有字段,我会添加一个新的构造函数并将字段注入其中。而不是这个:
public class ClassToTest {
private final String first = "first";
private final List<String> second = new ArrayList<>();
...
}
我会用这个:
public class ClassToTest {
private final String first;
private final List<String> second;
public ClassToTest() {
this("first", new ArrayList<>());
}
public ClassToTest(final String first, final List<String> second) {
this.first = first;
this.second = second;
}
...
}
即使使用一些遗留代码,这也不会成为问题。旧代码将使用一个空的构造函数,如果你问我,重构后的代码会看起来更干净,你将能够在测试中注入必要的值而无需反射。
现在关于私有方法。根据我的个人经验,当您必须对私有方法进行测试时,该方法与该类无关。在这种情况下,一个常见的模式是将它包装在一个接口中,就像Callable
然后你在构造函数中也传递该接口(使用多个构造函数技巧):
public ClassToTest() {
this(...);
}
public ClassToTest(final Callable<T> privateMethodLogic) {
this.privateMethodLogic = privateMethodLogic;
}
我写的大部分内容看起来都是依赖注入模式。以我个人的经验,它在测试时非常有用,而且我认为这种代码更干净,更容易维护。对于嵌套类,我也会这么说。如果嵌套类包含繁重的逻辑,最好将其作为包私有类移动并将其注入到需要它的类中。
在重构和维护遗留代码时,我还使用了其他几种设计模式,但这一切都取决于要测试的代码的情况。大多数情况下使用反射不是问题,但是当您有一个经过大量测试并且在每次部署之前运行测试的企业应用程序时,一切都会变得非常缓慢(这很烦人,我不喜欢那种东西)。
还有 setter 注入,但我不建议使用它。我最好坚持使用构造函数并在真正需要时初始化所有内容,从而留下注入必要依赖项的可能性。
PowerMockito 就是为此而生的。使用maven依赖
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
然后你可以做
import org.powermock.reflect.Whitebox;
...
MyClass sut = new MyClass();
SomeType rval = Whitebox.invokeMethod(sut, "myPrivateMethod", params, moreParams);
请参阅下面的示例;
应添加以下导入语句:
import org.powermock.reflect.Whitebox;
现在您可以直接传递具有私有方法、要调用的方法名称和附加参数的对象,如下所示。
Whitebox.invokeMethod(obj, "privateMethod", "param1");
这是我测试私有字段的通用函数:
protected <F> F getPrivateField(String fieldName, Object obj)
throws NoSuchFieldException, IllegalAccessException {
Field field =
obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return (F)field.get(obj);
}
私有方法只能在同一个类中访问。因此,无法从任何测试类测试目标类的“私有”方法。一个出路是您可以手动执行单元测试,或者可以将您的方法从“私有”更改为“受保护”。
然后只能在定义类的同一个包中访问受保护的方法。因此,测试目标类的受保护方法意味着我们需要在与目标类相同的包中定义您的测试类。
如果以上都不能满足您的要求,请使用反射方式访问私有方法。
正如上面许多人所建议的,一个好方法是通过您的公共接口测试它们。
如果您这样做,最好使用代码覆盖工具(如 Emma)来查看您的私有方法是否实际上是从您的测试中执行的。
今天推了一个Java库来帮助测试私有方法和字段。它的设计考虑了 Android,但它确实可以用于任何 Java 项目。
如果您有一些带有私有方法或字段或构造函数的代码,则可以使用BoundBox。它完全符合您的要求。下面是一个访问 Android 活动的两个私有字段以对其进行测试的测试示例:
@UiThreadTest
public void testCompute() {
// Given
boundBoxOfMainActivity = new BoundBoxOfMainActivity(getActivity());
// When
boundBoxOfMainActivity.boundBox_getButtonMain().performClick();
// Then
assertEquals("42", boundBoxOfMainActivity.boundBox_getTextViewMain().getText());
}
BoundBox使测试私有/受保护字段、方法和构造函数变得容易。你甚至可以访问被继承隐藏的东西。事实上,BoundBox 打破了封装。它将使您可以通过反射访问所有内容,但是在编译时会检查所有内容。
它非常适合测试一些遗留代码。小心使用它。;)
首先,我要抛出这个问题:为什么您的私人成员需要隔离测试?它们是否那么复杂,提供如此复杂的行为,以至于需要在公共表面之外进行测试?这是单元测试,而不是“代码行”测试。不要为小事出汗。
如果它们大到足以使这些私有成员在复杂性上都是一个“单元”——考虑将这些私有成员从这个类中重构出来。
如果重构不合适或不可行,是否可以在单元测试时使用策略模式来替换对这些私有成员函数/成员类的访问?在单元测试下,该策略将提供额外的验证,但在发布版本中,它将是简单的传递。
我最近遇到了这个问题,写了一个小工具,叫做Picklock,它避免了显式使用 Java 反射 API 的问题,两个例子:
调用方法,例如private void method(String s)
- 通过 Java 反射
Method method = targetClass.getDeclaredMethod("method", String.class);
method.setAccessible(true);
return method.invoke(targetObject, "mystring");
调用方法,例如private void method(String s)
- 通过 Picklock
interface Accessible {
void method(String s);
}
...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.method("mystring");
设置字段,例如private BigInteger amount;
- 通过 Java 反射
Field field = targetClass.getDeclaredField("amount");
field.setAccessible(true);
field.set(object, BigInteger.valueOf(42));
设置字段,例如private BigInteger amount;
- 通过 Picklock
interface Accessible {
void setAmount(BigInteger amount);
}
...
Accessible a = ObjectAccess.unlock(targetObject).features(Accessible.class);
a.setAmount(BigInteger.valueOf(42));
对于Java,我会使用反射,因为我不喜欢仅仅为了测试而改变对声明方法的包的访问的想法。但是,我通常只测试公共方法,这也应该确保私有方法正常工作。
您不能使用反射从所有者类外部获取私有方法,私有修饰符也会影响反射
这不是真的。正如Cem Catikkas 的回答中提到的那样,您当然可以。
我想分享一条关于测试的规则,该规则特别与该主题相关:
我认为你永远不应该为了更轻松地编写测试而调整生产代码。
其他帖子中有一些建议来调整原始类以测试私有方法。如果您将方法/字段的可访问性更改为包私有或受保护,只是为了让测试可以访问它,那么您就违背了私有访问指令存在的目的。
如果我们想要进行测试驱动开发,为什么我们应该拥有私有字段/方法/类?我们是否应该将所有内容都声明为私有包,甚至是公共包,以便我们可以毫不费力地进行测试?- 我不这么认为。
从另一个角度来看。测试不应对生产应用程序的性能和执行造成负担。如果您只是为了更容易测试而更改生产代码,那么您可能会以某种方式对性能和应用程序的执行造成负担。而且,如果您开始将私有访问更改为包私有,您最终可能会想出一些“巧妙的想法”,即在原始类中添加更多代码,这会给可读性带来额外的噪音,并会影响应用程序的性能。
另一方面,随着将私有访问更改为限制较少的一些,您正在为开发人员在应用程序的未来开发中滥用新情况提供可能性。不是强迫他以正确的方式发展,而是为他打开新的可能性,让他有能力在未来做出错误的选择。
当然,这条规则可能有一些例外,但要清楚地了解什么是规则,什么是例外以及为什么要这样做。
您可以关闭反射的 Java 访问限制,这样私有就没有任何意义。
setAccessible(true)
通话就是这样做的。
唯一的限制是 ClassLoader 可能不允许您这样做。
有关在 Java 中执行此操作的方法,请参阅Subverting Java Access Protection for Unit Testing (Ross Burton)。
在 c++ 中:在包含具有要测试的私有函数的类头之前
使用此代码:
#define private public
#define protected public
嘿,如果你在spring使用这个实用程序类。
ReflectionTestUtils.invokeMethod(new ClassName(), "privateMethodName");
我不确定这是否是一种好的技术,但我开发了以下模式来对私有方法进行单元测试:
我不会修改要测试的方法的可见性并添加其他方法。相反,我为我想要测试的每个私有方法添加了一个额外的公共方法。我调用这个附加方法Test-Port
并用前缀表示它们t_
。这个Test-Port
方法然后简单地访问相应的私有方法。
此外,我在方法中添加了一个布尔标志,Test-Port
以决定是否通过Test-Port
外部方法授予对私有方法的访问权限。然后在我放置应用程序的其他全局设置的静态类中全局设置此标志。所以我可以在一个地方打开和关闭对私有方法的访问,例如在相应的单元测试中。
使用 ExpectedException 时对@Cem Catikka 的评论的快速补充:
请记住,您预期的异常将包含在 InvocationTargetException 中,因此为了获得您的异常,您必须抛出您收到的 InvocationTargetException 的原因。类似于(在 BizService 上测试私有方法 validateRequest()):
@Rule
public ExpectedException thrown = ExpectedException.none();
@Autowired(required = true)
private BizService svc;
@Test
public void testValidateRequest() throws Exception {
thrown.expect(BizException.class);
thrown.expectMessage(expectMessage);
BizRequest request = /* Mock it, read from source - file, etc. */;
validateRequest(request);
}
private void validateRequest(BizRequest request) throws Exception {
Method method = svc.getClass().getDeclaredMethod("validateRequest", BizRequest.class);
method.setAccessible(true);
try {
method.invoke(svc, request);
}
catch (InvocationTargetException e) {
throw ((BizException)e.getCause());
}
}
我想贡献一下你们中的许多人担心的不测试私有方法的问题,考虑到代码覆盖工具将准确地确定测试了多少代码以及泄漏的位置,因此可以量化地接受做这个。
我要说一些需要说的东西,但很多人可能不喜欢听。你们中那些将问题的作者引向“变通”的答案的人正在对社区造成巨大的伤害。测试是所有工程学科的重要组成部分,您不会想要购买未经适当测试且以有意义的方式测试的汽车,那么为什么会有人想要购买或使用测试不佳的软件呢?无论如何,人们这样做的原因可能是因为经过糟糕测试的软件的影响是事后才感觉到的,我们通常不会将它们与身体伤害联系起来。这是一种非常危险的看法,很难改变,但我们有责任提供安全的产品,无论管理层都在欺负我们做什么。想想 Equifax 黑客...
我们必须努力建立一个鼓励良好的软件工程实践的环境,这并不意味着排斥我们中间那些不认真对待他们的手艺的弱者/懒惰者,而是要创造一种能够鼓励每个人的责任感和自我反思的现状追求成长,无论是精神上还是技巧上。我仍在学习,我自己可能有错误的看法/意见,但我坚信我们需要对良好实践负责,避免不负责任的黑客攻击或解决问题的方法。
用 lombok 详细说明我的示例,如下所示。私有字段,私有方法:
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Student student = new Student();
Field privateFieldName = Student.class.getDeclaredField("name");
privateFieldName.setAccessible(true);
privateFieldName.set(student, "Naruto");
Field privateFieldAge = Student.class.getDeclaredField("age");
privateFieldAge.setAccessible(true);
privateFieldAge.set(student, "28");
System.out.println(student.toString());
Method privateMethodGetInfo = Student.class.getDeclaredMethod("getInfo", String.class, String.class);
privateMethodGetInfo.setAccessible(true);
System.out.println(privateMethodGetInfo.invoke(student, "Sasuke", "29"));
}
@Setter
@Getter
@ToString
class Student {
private String name;
private String age;
private String getInfo(String name, String age) {
return name + "-" + age;
}
}
在 C# 中你可以使用System.Reflection
,但在 Java 中我不知道。尽管无论如何我都有回答这个问题的冲动,因为如果您“觉得需要对私有方法进行单元测试”,我的猜测是还有其他问题……
我会认真考虑用新的眼光再次审视我的建筑......
如果您的测试类与应测试的类在同一个包中怎么办?
但是当然在不同的目录中,src
&classes
用于您的源代码test/src
和test/classes
您的测试类。让classes
并test/classes
在你的类路径中。
从测试框架测试 Java 私有方法的最佳和正确的合法方法是在方法上使用@VisibleForTesting注释,因此相同的方法对于测试框架将与公共方法一样可见。
PowerMock.Whitebox
是我见过的最好的选择,但是当我阅读它的源代码时,它会通过反射读取私有字段,所以我想我有我的答案:
一段时间后,当我重新考虑它时,我仍然相信这是真的,但我看到了更好的方法。
首先,Powermock.Whitebox
还是可以用的。
而且,MockitoWhitebox
在 v2(我能找到的最新版本Whitebox
是testImplementation 'org.mockito:mockito-core:1.10.19'
)之后被隐藏了,它一直是org.mockito.internal
包的一部分,将来很容易发生重大变化(见这篇文章)。所以现在我倾向于不使用它。
在 Gradle/Maven 项目中,如果您定义私有方法或字段,则除了反射之外没有其他方法可以访问它们,因此第一部分保持不变。但是,如果您将可见性更改为“包私有”,则包中遵循相同结构的测试test
将可以访问它们。这也是鼓励我们在main
和test
包中创建相同层次结构的另一个重要原因。因此,当您可以控制生产代码和测试时,删除该private
访问修饰符可能是您的最佳选择,因为它相对不会造成巨大影响。而且,这使得测试以及私有方法间谍成为可能。
@Autowired
private SomeService service; // with a package private method "doSomething()"
@Test
void shouldReturnTrueDoSomething() {
assertThat(doSomething(input), is(true)); // package private method testing
}
@Test
void shouldReturnTrueWhenServiceThrowsException() {
SomeService spy = Mockito.spy(service); // spying real object
doThrow(new AppException()).when(spy).doSomething(input); // spy package private method
...
}
当涉及到内部字段时,在 Spring 中你有ReflectionUtils.setField()
.
最后,有时我们可以绕过问题本身:如果需要满足覆盖要求,也许您可以将这些私有方法移到内部静态类中,并在 Jacoco 中忽略该类。我刚刚找到了一些忽略 Jacoco gradle 任务中的内部类的方法。另一个问题
我认为大多数答案都太粗糙了。是否应该测试私有方法取决于您的代码。
我过去使用私有方法测试,我通过反思来做到这一点。这个对我有用。我意识到它的问题,但对我来说这是最好的解决方案。
我有一个大型应用程序,可以模拟超过一百万人口中的人类行为。每个人都由一个对象代表。该应用程序的主要目的是跟踪某些事物(疾病、信息、想法)如何在人群中传播。
为此,我有将疾病或信息从一个对象传递到另一个对象的方法。绝对没有理由公开这些方法,因为最终用户对人与人之间的一次传递不感兴趣。最终用户只对它如何在人群中传播的宏大图景感兴趣。所以这些方法是私有的。
但我想确定将一点信息从一个人传递给另一个人的单一行为是否正在做它应该做的事情。通过公共用户界面对其进行测试也是不可能的,因为它只是不公开的,而且我认为仅仅为了测试目的而将其公开是很尴尬的。应用程序的最终输出由始终执行的数亿个此类单个步骤定义。我也无法测试最终输出,因为这涉及复杂的随机性,这使得测试变得难以预测。
因此,我测试应用程序的方法是测试将信息从一个人传递给另一个人的单个步骤。这些都是私有方法。所以我使用反射来测试它。
我讲这个故事是为了表明这不是一个简单的黑白故事。这取决于您的应用程序什么是最好的。在某些情况下,通过反射测试私有方法可能是您的最佳选择。
如果这里的某些人在我的用例中知道更好的解决方案,我当然会很乐意纠正......
JML 有一个 spec_public 注释注释语法,允许您在测试期间将方法指定为公共的:
private /*@ spec_public @*/ int methodName(){
...
}
此语法在http://www.eecs.ucf.edu/~leavens/JML/jmlrefman/jmlrefman_2.html#SEC12进行了讨论。还有一个将 JML 规范转换为 JUnit 测试的程序。我不确定它的效果如何或它的功能是什么,但它似乎没有必要,因为 JML 本身就是一个可行的测试框架。
您可以使用 PowerMockito 为要测试的私有方法中调用/使用的私有字段和私有方法设置返回值:
例如。设置私有方法的返回值:
MyClient classUnderTest = PowerMockito.spy(new MyClient());
//Set expected return value
PowerMockito.doReturn(20).when(classUnderTest, "myPrivateMethod", anyString(), anyInt());
//This is very important otherwise it will not work
classUnderTest.myPrivateMethod();
//Setting private field value as someValue:
Whitebox.setInternalState(classUnderTest, "privateField", someValue);
最后,您可以通过以下方式验证您的私有方法是否根据上面的设置值返回正确的值:
String msg = Whitebox.invokeMethod(obj, "privateMethodToBeTested", "param1");
Assert.assertEquals(privateMsg, msg);
或者
如果 classUnderTest 私有方法没有返回值,但它设置了另一个私有字段,那么您可以获取该私有字段值以查看它是否设置正确:
//To get value of private field
MyClass obj = Whitebox.getInternalState(classUnderTest, "foo");
assertThat(obj, is(notNull(MyClass.class))); // or test value
如果您确实需要直接测试私有方法/类等,则可以使用其他答案中已经提到的反射。但是,如果涉及到这一点,我宁愿使用框架提供的 util 类,而不是直接处理反射。例如,对于 Java,我们有:
ReflectionUtils Spring 框架
ReflectionUtils JUnit
根据如何使用它们,您可以在网上找到大量文章。这是我特别喜欢的一个:
您可以创建一个特殊的公共方法来代理私有方法进行测试。使用 IntelliJ 时,@TestOnly 注释是开箱即用的。缺点是如果有人想在公共上下文中使用私有方法,他可以做到。但是他会被注解和方法名警告。在 IntelliJ 上,执行此操作时会出现警告。
import org.jetbrains.annotations.TestOnly
class MyClass {
private void aPrivateMethod() {}
@TestOnly
public void aPrivateMethodForTest() {
aPrivateMethod()
}
}
Android有来自包的@VisibleForTesting
注释。android.support.annotation
@VisibleForTesting
注释表明带注释的方法比使该方法可测试所需的通常更可见。此注释有一个可选otherwise
参数,可让您指定该方法的可见性(如果不是为了使其可见以进行测试)。Lint 使用该otherwise
参数来强制实现预期的可见性。
在实践中,这意味着您应该打开一个方法进行测试,并且@VisibleForTesting
注释将显示警告。
例如
package com.mypackage;
public class ClassA {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static void myMethod() {
}
}
当您在同一个包(com.mypackage)中调用 ClassA.myMethod() 时,您将看到警告。
如果您只使用 Mockito:
您可以将私有方法视为正在测试的公共方法的一部分。在测试公共方法时,您可以确保覆盖私有方法中的所有情况。
假设您是 mockito 唯一用户(您不允许或不想使用 Powermock 或反射或任何此类工具)并且您不想更改正在测试的现有代码或库,这可能是最好的方法。
如果您选择这种方式,您唯一需要处理的是在私有方法中本地声明的变量(用户定义的对象)。如果私有方法依赖于本地声明的变量对象及其方法,请确保将这些用户定义的对象全局声明为私有对象,而不是本地声明的对象。您可以在本地实例化这些对象。
这允许您模拟这些对象并将它们注入回测试对象。您还可以模拟(使用 when/then)他们的方法。
这将允许您在测试公共方法时测试私有方法而不会出错。
优点 1. 代码覆盖率 2. 能够测试完整的私有方法。
缺点 1. 对象的范围 - 如果您不希望对象暴露给同一类中的其他方法,这可能不是您的方式。2. 当在不同的公共方法和/或在同一个方法中多次调用时,您最终可能会多次测试私有方法。
我只测试公共接口,但众所周知我会保护特定的私有方法,因此我可以完全模拟它们,或者添加特定于单元测试目的的额外步骤。一般情况是挂钩我可以从单元测试中设置的标志,以使某些方法故意导致异常能够测试故障路径;异常触发代码仅在受保护方法的覆盖实现中的测试路径中。
不过,我尽量减少对此的需求,并且我总是记录确切的原因以避免混淆。
Groovy 有一个bug/feature,通过它您可以调用私有方法,就好像它们是公共的一样。因此,如果您能够在项目中使用 Groovy,那么您可以使用它来代替反射。查看此页面以获取示例。
对于 C++(自 C++11 起),将测试类添加为朋友可以完美运行,并且不会破坏生产封装。
让我们假设我们有一些Foo
具有一些真正需要测试的私有函数的类,以及一些FooTest
应该可以访问 Foo 的私有成员的类。那么我们应该写如下:
// prod.h: some production code header
// forward declaration is enough
// we should not include testing headers into production code
class FooTest;
class Foo
{
// that does not affect Foo's functionality
// but now we have access to Foo's members from FooTest
friend FooTest;
public:
Foo();
private:
bool veryComplicatedPrivateFuncThatReallyRequiresTesting();
}
// test.cpp: some test
#include <prod.h>
class FooTest
{
public:
void complicatedFisture() {
Foo foo;
ASSERT_TRUE(foo.veryComplicatedPrivateFuncThatReallyRequiresTesting());
}
}
int main(int /*argc*/, char* argv[])
{
FooTest test;
test.complicatedFixture(); // and it really works!
}
以下Reflection TestUtil可用于测试私有方法的原子性。
import com.google.common.base.Preconditions;
import org.springframework.test.util.ReflectionTestUtils;
/**
* <p>
* Invoker
* </p>
*
* @author
* @created Oct-10-2019
*/
public class Invoker {
private Object target;
private String methodName;
private Object[] arguments;
public <T> T invoke() {
try {
Preconditions.checkNotNull(target, "Target cannot be empty");
Preconditions.checkNotNull(methodName, "MethodName cannot be empty");
if (null == arguments) {
return ReflectionTestUtils.invokeMethod(target, methodName);
} else {
return ReflectionTestUtils.invokeMethod(target, methodName, arguments);
}
} catch (Exception e) {
throw e;
}
}
public Invoker withTarget(Object target) {
this.target = target;
return this;
}
public Invoker withMethod(String methodName) {
this.methodName = methodName;
return this;
}
public Invoker withArguments(Object... args) {
this.arguments = args;
return this;
}
}
Object privateMethodResponse = new Invoker()
.withTarget(targetObject)
.withMethod(PRIVATE_METHOD_NAME_TO_INVOKE)
.withArguments(arg1, arg2, arg3)
.invoke();
Assert.assertNotNutll(privateMethodResponse)
我感觉完全一样...更改方法的访问修饰符只是为了能够运行测试,这对我来说是个坏主意。同样在我们公司,我们对此进行了很多讨论,在我看来,测试私有方法的真正好方法是使用 java 反射或其他使方法可测试的框架。对于复杂的私有方法,我多次这样做,这有助于我保持测试的小型化、可读性和可维护性。
在我在这里阅读了所有答案之后,我不同意那些说“如果你需要测试私有方法,那么就会有代码味道”甚至“不要测试私有方法”的人......所以我有一个给你的小例子:
想象一下,我有一个带有一个公共方法和几个私有方法的类:
public class ConwaysGameOfLife {
private boolean[][] generationData = new boolean[128][128];
/**
* Compute the next generation and return the new state
* Also saving the new state in generationData
*/
public boolean[][] computeNextGeneration() {
boolean[][] tempData = new boolean[128][128];
for (int yPos=0; yPos<=generationData.length; yPos++) {
for (int xPos=0; xPos<=generationData[yPos].length; xPos++) {
int neighbors = countNeighbors(yPos, xPos);
tempData[yPos][xPos] = determineCellState(neighbors, yPos, xPos);
}
}
generationData = tempData;
return generationData;
}
/**
* Counting the neighbors for a cell on given position considering all the edge cases
*
* @return the amount of found neighbors for a cell
*/
private int countNeighbors(int yPos, int xPos) {}
/**
* Determine the cell state depending on the amount of neighbors of a cell and on a current state of the cell
*
* @return the new cell state
*/
private boolean determineCellState(int neighborsAmount, int yPos, int xPos) {}
}
因此,至少对于“countNeighbors”方法,我需要测试 8 个边缘情况以及一些一般情况(单元格直接位于角落,单元格直接位于矩阵边缘,单元格位于中间某处)。因此,如果我只是试图通过“computeNextGeneration”方法覆盖所有情况,并且在重构之后,一些测试是红色的,那么确定错误所在的位置可能很耗时。如果我分别测试“determineCellState”和“countNeighbors”并且在重构和优化之后,“computeNextGeneration”和“determineCellState”的测试是红色的,我很确定错误将出现在“determineCellState”方法中。
此外,如果您从一开始就为这些方法编写单元测试,这些测试将帮助您开发方法/算法,而无需考虑并将其他方法调用和案例包装在公共方法中。您可以在该方法中编写涵盖您的案例的快速小型测试......例如,如果名称为“countNeighbors_should_return_right_amount_of_neighbors_for_the_right_top_corner_cell()”的测试失败,那么很清楚在哪里寻找错误
还有另一种方法可以测试您的私有方法。如果您在运行配置中“启用断言”,那么您可以在方法本身内对您的方法进行单元测试。例如;
assert ("Ercan".equals(person1.name));
assert (Person.count == 2);
在你的课堂上:
namespace my_namespace {
#ifdef UNIT_TEST
class test_class;
#endif
class my_class {
public:
#ifdef UNIT_TEST
friend class test_class;
#endif
private:
void fun() { cout << "I am private" << endl; }
}
}
在您的单元测试类中:
#ifndef UNIT_TEST
#define UNIT_TEST
#endif
#include "my_class.h"
class my_namespace::test_class {
public:
void fun() { my_obj.fun(); }
private:
my_class my_obj;
}
void my_unit_test() {
test_class test_obj;
test_obj.fun(); // here you accessed the private function ;)
}