powermock
本周,我注意到在整个测试中使用了多少PowerMock来模拟静态或私有方法。 在一个特定的程序包中,删除它可以将测试执行时间缩短一个数量级(从大约20秒减少到2秒)。 这显然是滥用:我看到了使用PowerMock的三个主要原因。
缺乏API知识
可能一定有很好的理由,但是如果开发人员仅检查了基础代码,则可以避免使用某些PowerMock。 此类代码的一个示例如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
publicclassExampleTest{
@MockprivateSecurityContextsecurityContext;
publicvoidsetUp()throwsException{
mockStatic(SecurityContextHolder.class);
when(SecurityContextHolder.getContext()).thenReturn(securityContext);
}
// Rest of the test
}
快速浏览一下Spring的SecurityContextHolder
就会发现它具有setContext()
方法,因此可以轻松地用以下代码替换之前的代码段:
@RunWith(MockitoJUnitRunner.class)
publicclassExampleTest{
@MockprivateSecurityContextsecurityContext;
publicvoidsetUp()throwsException{
SecurityContextHolder.setContext(securityContext);
}
// Rest of the test
}
我注意到的另一个常见代码段如下:
@RunWith(PowerMockRunner.class)
@PrepareForTest(WebApplicationContextUtils.class)
publicclassExampleTest{
@MockprivateWebApplicationContextwac;
publicvoidsetUp()throwsException{
mockStatic(WebApplicationContextUtils.class);
when(WebApplicationContextUtils.getWebApplicationContext(any(ServletContext.class))).thenReturn(wac);
}
// Rest of the test
}
虽然比上一个示例稍难一些,但是查看WebApplicationContextUtils
的源代码可以发现它针对上下文是servlet上下文。
可以轻松更改测试代码以删除PowerMock:
@RunWith(MockitoJUnitRunner.class)
publicclassExampleTest{
@MockprivateWebApplicationContextwac;
@MockprivateServletContextsc;
publicvoidsetUp()throwsException{
when(sc.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)).thenReturn(wac);
}
// Rest of the test
}
可见度太严格
如上所示,良好的框架(例如Spring)使在测试中易于使用它们。 不幸的是,我们的代码不能总是如此。 在这种情况下,我通过扩大方法和类从私有(或包)到公共的可见性来删除了PowerMock。
您可能会辩解说,破坏封装来改进测试是错误的,但是在这种情况下,我倾向于鲍勃叔叔的观点:
测试王牌封装。
https://blog.8thlight.com/uncle-bob/2015/06/30/the-little-singleton.html
实际上,您认为封装可以防止其他开发人员滥用您的代码。 但是,您可以在测试中通过反射来打破它...是什么保证开发人员不会在生产代码中使用反射方式呢?
务实的解决方案是对您的设计进行一些折衷,但这是问题的核心,将其记录下来 。 Guava和Fest库都具有@VisibleForTesting
批注,我觉得很方便。 锦上添花的是IDE可以识别它,而不是在src/main/java
文件夹中建议自动完成。
直接使用静态方法
最后一点已经被解释了一次又一次,但是一些开发人员仍然不能正确地应用它。 一些非常常见的API仅提供静态方法,而没有其他选择, 例如 Locale.getDefault()
或Calendar.getInstance()
。 此类方法不应直接在您的生产代码上调用,否则它们将使您的设计只能通过PowerMock进行测试。
publicclassUntestableFoo{
publicvoiddoStuff(){
Calendarcal=Calendar.getInstance();
// Do stuff on calendar;
}
}
@RunWith(PowerMock.class)
@PrepareForTest(Calendar.class)
publicclassUntestableFooTest{
@MockprivateCalendarcal;
privateUntestableFoofoo;
@Before
publicvoidsetUp(){
mockStatic(Calendar.class);
when(Calendar.getInstance()).thenReturn(cal);
// Stub cal accordingly
foo=newUntestableFoo();
}
// Add relevant test methods
}
要解决此设计缺陷,只需使用注入,更精确地使用构造函数注入:
publicclassTestableFoo{
privatefinalCalendarcalendar;
publicTestableFoo(Calendarcalendar){
this.calendar=calendar;
}
publicvoiddoStuff(){
// Do stuff on calendar;
}
}
@RunWith(MockitoJUnitRunner.class)
publicclassTestableFooTest{
@Mock
privateCalendarcal;
privateTestableFoofoo;
@Before
publicvoidsetUp(){
// Stub cal accordingly
foo=newTestableFoo(cal);
}
// Add relevant test methods
}
此时,唯一的问题是如何首先创建实例。 很容易,具体取决于您的注入方法:Spring @Bean
方法, CDI @Inject Provider<T>
方法或使用您自己的方法之一调用getInstance()
方法。 这是Spring的方式:
@Configuration
publicclassMyConfiguration{
@Bean
publicCalendarcalendar(){
returnCalendar.getInstance();
}
@Bean
publicTestableFoofoo(){
returnnewTestableFoo(calendar());
}
}
结论
PowerMock是一个非常强大且有用的工具。 但是,仅在绝对必要时才应使用它,因为它会对测试执行时间产生巨大影响。 在本文中,我试图说明如何在3个不同的用例中不使用它:缺乏API知识,过于严格的可见性和直接的静态方法调用。 如果您发现测试代码库中充满了PowerMock的用法,建议您尝试上述方法以摆脱它们。
我从来都不是TDD的粉丝(可能是另一篇文章的主题),但是我相信如果使用TDD,可以很容易地避免最后两点。
powermock