是我在使用 Powermock 中逐渐归纳的一些使用方法、遇到的坑、以及不同场景的最佳实践。文章没有对 PowerMock 这个框架做非常详细的介绍,但已经足够使用了。缺点是没有那种很长很全面的案例,因为我不想让文章看起来太长太复杂。但全部读完再动手实践一下,应该能应对 99% 的场景了。
Mockito 与 PowerMock 都是 Java 流行的 Mock 框架,使用Mock技术能让我们隔离外部依赖以便对我们自己的业务逻辑代码进行单元测试。
Mockito 的工作原理是通过创建依赖对象的 proxy,所有的调用先经过 proxy 对象,proxy 对象拦截了所有的请求再根据预设的返回值进行处理。
PowerMock 则在 Mockito 原有的基础上做了扩展,通过修改类字节码并使用自定义 ClassLoader 加载运行的方式来实现 mock 静态方法、final 方法、private 方法、系统类的功能。
从两者的项目结构中就可以看出,PowerMock 直接依赖于 Mockito,所以在项目中使用时只需要导入 PowerMock 包,不需要再单独导入 Mockito 包。并且 PowerMock 完全支持了 Mocktio 的 API,又在此基础上增加了自己的 API,可以看作是 Mocktio 的增强版。
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
常用在类上面的注解有以下几个:
@RunWith
在测试类类名上添加 @RunWith(PowerMockRunner.class) 注解代表该测试类使用 PowerMock。必须添加
@PrepareForTest
这个注解的作用就是告诉 PowerMock 哪些类是需要在字节码级别上进行操作的。也就是需要 mock 某些包含 final、static 等方法的类时使用,使用方法:@PrepareForTest({System.class, LogUtils.class})
@PowerMockIgnore
PowerMock 是使用自定义的类加载器来加载被修改过的类,从而达到打桩的目的。@PowerMockIgnore 注解告诉 PowerMock 忽略哪些包下的类,从而消除类加载器引入的 ClassCastException。使用方法:@PowerMockIgnore({“javax.management.*”, “javax.net.ssl.*”, “javax.script.*”})
@SuppressStaticInitializationFor
告诉 PowerMock 哪些包下的类需要被抑制静态初始化,包括 static 代码块或者 static 变量的初始化。防止因静态初始化导致的错误。使用方法:
@SuppressStaticInitializationFor({“com.xxx.SmsServiceImpl”})
常用在字段/属性上面的注解有以下几个:
@InjectMocks
创建一个待测类的实例,其余用 @Mock 注解创建的 mock 对象都将被自动注入到用该实例中。
Spring 使用 @Autowird 完成自动注入。但在单元测试中,没有启动 spring 框架,此时就需要通过 @ InjectMocks 完成依赖注入。@InjectMocks 会将带有 @Mock 注解的对象注入到待测试的目标类中。
@Mock
创建一个 mock 对象。
使用 @InjectMocks 和 @Mock 配合能 mock 出被 Spring 容器托管的 bean,并自动注入到待测试类中。
@RunWith(PowerMockRunner.class)
@PrepareForTest({})
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*", "javax.script.*"})
public class MyServiceTest {
@InjectMocks
private MyService testInstance; //待测类
@Mock
private MyDao myDao; //mock 对象:myDao 会被自动注入到 testInstance 中
}
常用在方法上面的注解有以下几个:
@Before
初始化方法,对于每一个测试方法都要执行一次,一般用于测试前的属性赋值、mock 待测试方法等。注意和 @BeforeClass 的区别,后者是必须为 static void,只执行一次,针对所有方法。
@Test
@Test 放在方法上表示这是一个测试方法。
PowerMock/Mocktio 在 mock 方法时是采用 when…thenDo… 的逻辑,如
PowerMocktio.when(myDao.queryById(ArgumentMatchers.any(Long.class))).thenReturn(new Entity());
上面代码表示:当执行 myDao 对象的 queryById() 方法时,只要改方法接受的参数是 Long 类型,无论参数的值是什么,都返回一个 new Entity() 对象。
类似的执行逻辑还有:when().thenThrow() 抛出指定异常; when().thenAnswer() 执行自定义逻辑。
如果执行的方法无返回值,不需要抛异常,仅仅只是想要 mock 掉执行过程,则可以 doNothing().when()
类似的参数通配符有:anyInt()、anyString()、anyObject()、isA()、isNull()、notNull()、eq()…
推荐在导包时加入:
import static org.mockito.ArgumentMatchers.*;
import static org.powermock.api.mockito.PowerMockito.*;
就可以免去写 PowerMocktio 和 ArgumentMatchers,直接调用 when()、any()…,以及 mock()、mockStatic()…
方法一:@Mock
mock 待测类的普通属性,最常见的就是通过 Spring @Autowired 自动注入的 bean。这类属性并非 final,也并非 static,只需要在测试类中使用 @Mock 注解,Pwermock 框架就能自动生成 mock 对象,并自动注入到 @InjectMock 修饰的待测类中。
方法二:mock()
也可以使用比较通用的 mock() 方法,调用 mock(XXX.class),能生成一个 mock 的 XXX 对象,然后通过反射获取待测类的字段,再将 mock 对象赋给字段。一般用于不是由 Spring @Autowired 注入的普通属性。
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*", "javax.script.*"})
public class MyServiceTest {
@InjectMocks
private MyService testInstance;
@Mock
private MyDao myDao; //方法一
// @Before 方法会在所有待测方法前执行一次
// 对于字段的赋值、或者某些返回值是固定的方法的 mock,建议在这里做
@Before
public void init() throws Exception {
RedisUtils redisUtilsMock = mock(RedisUtils.class); //方法二
Field redisUtilsField = testInstance.getClass().getDeclaredField("redisUtils"); //使用反射获取待测类的字段
redisUtilsField.setAccessible(true);
redisUtilsField.set(testInstance, redisUtilsMock);//赋值
}
}
PowerMock/Mocktio 在 mock 方法时是采用 when…thenDo… 的逻辑。在 mock 了对象之后,PowerMock 会自动 mock 该对象内的普通方法,不过是执行结果都是固定的,如果要自定义 mock 方法。可以如下:
when(myDao.queryById(any(Long.class))).thenReturn(new Entity());
上面代码表示:当执行 myDao 对象的 queryById() 方法时,只要该方法接受的参数是 Long 类型,无论参数的值是什么,都返回一个 new Entity() 对象。
类似的执行逻辑还有:when().thenThrow() 抛出指定异常; when().thenAnswer() 执行自定义逻辑。
如果执行的方法无返回值,不需要抛异常,仅仅只是想要 mock 掉执行过程,则可以 doNothing().when()
类似的参数通配符有:anyInt()、anyString()、anyObject()、isA()、isNull()、notNull()、eq()…
参考普通属性
需要在 @PrepareForTest 注解中添加要 mock 的类
@PrepareForTest({MyDao.class})
方法一:
对于 private 方法,因为无法在外界直接访问,就不能像普通方法那样 mock,应该使用下面的方法,不使用对象直接调用,而是通过方法名字去调用:
when(myDao, "queryById", anyLong()).thenReturn(new Entity());
这是一种通用的方法。可以 mock private 方法,也可以 mock 普通方法。
方法二:
MemberModifier.stub()
方法一是 mock 某个对象的方法,而方法二则直接在类的层次上进行 mock:
// 使用这种方法的时候,待 mock 方法的参数不是使用通配符 anyLong(),而是使用泛型 Long.class
MemberModifier.stub(MemberMatcher.method(MyDao.class, "queryById", Long.class)).toReturn(new Entity());
这是一种更通用的方法,可用于待测类本身、父类、其他类的方法等等。由于是在类的层次上进行 mock,所以常用于 mock 没有实例的方法,比如通过继承获得的父类的方法,或者直接通过类名调用的 static 方法。当然,也可以用于 mock 实例的方法。
注意:如果代码覆盖率检测工具使用的是 jacoco,则无法 mock 待测类自身,因为类要放到 @PrepareForTest 注解中,而 jacoco 会忽略掉该注解中的类,覆盖率会为 0
需要在 @PrepareForTest 注解中添加要 mock 的类
参考普通属性
需要在 @PrepareForTest 注解中添加要 mock 的类
参考 private 方法和普通方法
需要在 @PrepareForTest 注解中添加要 mock 的类
在写单元测试时,一般时是通过待测类的实例来进行测试的,但是 static 属性并不属于实例。一般如果是需要对 static 属性中的某一两个方法进行 mock,则可以直接使用前面介绍的 private 方法中的 MemberModifier.stub() 来实现。
但如果有很多方法或者所有方法都需要 mock,并且没有什么定制化需求,直接让 PowerMock 自动实现,则可以通过下面的方法:
Whitebox.setInternalState()
LogUtils logUtilsMock = mock(LogUtils.class);
Whitebox.setInternalState(testInstance.getClass(), "logUtils", logUtilsMock);
然后对于 static 属性中的普通或者 private 方法的 mock,参考普通方法或者 private 方法
需要在 @PrepareForTest 注解中添加要 mock 的类
对于 static 方法的 mock,PowerMock 提供了一个 mockStatic() 方法,该方法对需要 mock static 方法的类中所有 static 方法进行自动 mock。
// 将 SendEmailProxy 类中的所有 static 方法 mock 掉
mockStatic(SendEmailProxy.class);
一般情况这个注解就够用,如果需要自定义 static 方法的返回值,参考 private 方法的方法二
需要在 @PrepareForTest 注解中添加要 mock 的类
一般很少 mock new 对象,但如果遇到待测方法内出现 new 依赖的对象,则需要对 new 对象也进行 mock。
Entity entity = new Entity();
whenNew(Entity.class).withAnyArguments().thenReturn(entity);
注意:由于要将需要 mock new 的类加入 @PrepareForTest 中,所以如果代码覆盖率检测工具使用的是 jacoco,则无法 mock 待测类自身
前面介绍了其他类的 private 方法的 mock。但有时候待测类里也会有一些 private 方法,而这些 private 方法 一般是给 public 方法调用的。比如有一个 private X 方法,而其他的 public A、B、C 三个方法都会调用到 X 方法。
所以我们需要把这个 private 方法也 mock 掉,但是由于要将需要 mock 的类加入 @PrepareForTest 直接中,而代码覆盖率检测工具使用的是 jacoco 时是无法 mock 待测类自身的,所以对于待测类本身的 private 方法,无法做到一步就直接 mock 掉,只能对 private 方法里面的内容进行 mock,以达到 mock 整个 private 方法的效果。
推荐的方式是在测试类中自定义一个方法,这个方法中对待测类的 private 方法中需要 mock 的地方进行 mock,并且该方法接受的参数就是待测类返回的参数,这样在每个需要 mock private 方法的地方,调用这个方法,就能实现“一步”完成 mock,并且还能自定义返回值。
该方法比较通用,对于待测类的所有需要 mock 的方法,都可以这样做。
mock 完成后,就可以调用待测试的方法进行测试。
一般来说,待测类和测试类是放在同一个包下的,所以 public、protected、缺省的方法都是可以通过待测类的实例直接调用来执行的。
static 方法则直接通过类名来调用。
通用的方法(包括 private)是通过反射获取待测方法执行。
PowerMock 的 when…do… 支持指定多次调用的返回值。如
when(myDao.queryById(any(Long.class))).thenReturn(new Entity(), null, new SQLException());
上面代码表示当调用 myDao.queryById() 时,第一次调用返回一个 Entity 对象,第二次返回 null,第三次抛出异常。
对于多分支的测试而言,就可以不需要写多个方法,而是在一个方法里完成多条分支的测试。