上一章讲了Spring-boot的starter test使用mock的方式mockito。但是mockito由于实现方式的原因(动态代理)不能支持静态、final、私有方法的mock。其实还有一种叫native方法,只是一般自己写native方法的地方不多,可能Android系统在这方面使用较多,比如游戏。查询了一些资料与笔者的以往经历,主要使用的有powerMock与jMockit。
powerMock在以前使用较多,最近反而使用少了,根本原因是不支持Junit5。官方最新版只支持Junit4,见官方文档
描述的很清楚,支持junit4版本,或者testNG,笔者在maven仓库看到junit5的支持jar,但是没人使用,更恶心的是有个版本号居然不能显示,看起来不是官方推送的。
所以笔者使用testNG来测试
pom文件如下,笔者依赖testNG与AssertJ。
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-testng</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.15.0</version>
<scope>test</scope>
</dependency>
随意写一个方法,包含静态、私有、final方法。如果是final类,方式同final方法。
package com.feng.demo;
public class User {
private String name;
public String getResult(String test){
return test + "\tjunit";
}
private String getPrivateName(String test){
return "123";
}
public final String getFinalName(String str) {
return "1235" + str;
}
public static String getStaticName(String string) {
return "12356" + string;
}
}
构建test方法,testNG需要继承PowerMockTestCase。
package com.feng.demo;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.testng.PowerMockTestCase;
import org.powermock.reflect.Whitebox;
import org.testng.annotations.Test;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
@PrepareForTest(User.class)//final方法准备,final类同理
public class UserTest extends PowerMockTestCase {
@Mock
private User user;
@Test
public void testTestGetResult() {
//由于次案例使用mockito,为powermock扩展,普通mock等同于mockito
PowerMockito.when(user.getResult(anyString())).thenReturn("powerMock");
String result = user.getResult("tom");
assertThat(result).isEqualTo("powerMock");
}
@Test
public void testGetFinalName() {
//final方法mock;这里同时mock了参数,跟final无关
PowerMockito.when(user.getFinalName(argThat(s -> true))).thenReturn("powerMock");
String result = user.getFinalName("tom");
assertThat(result).isEqualTo("powerMock");
}
@Test
public void testGetStaticName() {
//静态方法mock,先要设置需要mock的静态类
PowerMockito.mockStatic(User.class);
PowerMockito.when(User.getStaticName(argThat(s -> true))).thenReturn("powerMock");
String result = User.getStaticName("tom");
assertThat(result).isEqualTo("powerMock");
}
@Test
public void testGetPrivateName() throws Exception {
//mock私有方法
PowerMockito.when(user, "getPrivateName", anyString()).thenReturn("powerMock");
//私有方法实现单元测试,本质是反射调用
Method method = PowerMockito.method(User.class, "getPrivateName", String.class);
Object result = method.invoke(user, "12");
assertThat(result).isEqualTo("powerMock");
Object say = Whitebox.invokeMethod(user, "getPrivateName", "12");
assertThat(result).isEqualTo("powerMock");
}
}
重点介绍jmockit,功能十分强悍,只是测试代码有点不美观。不知道Spring-Boot推荐mockito而不是jmockit的原因是否是这个。jmockit查资料说支持mock私有方法,但笔者通过1.49版本测试是不支持的。
jmockit的本质Record-Replay-Verification
jmockit更新不是很频繁,最近更新是2019-12的1.49版本,支持junit5,这是jmockit1版本,一直在更新。
而且作者有jmockit2项目不知为啥2017年后不维护了。
pom依赖
<!-- Jmockit -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.49</version>
<scope>test</scope>
</dependency>
<!-- junit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.15.0</version>
<scope>test</scope>
</dependency>
继续使用上文的User作为需要测试的类。
这里要注意,仅仅依赖jar在maven的test是不管用的,笔者调试发现JMockit在执行单元测试时没有初始化,甚至笔者@ExtendWith(JMockitExtension.class)注入类都直接报错了。
查询官方文档:jmockit doc
笔者加入插件立马正常了,笔者查询很多博客,都没有这个,但根据笔者实践与官方文档是需要的。
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
</argLine>
</configuration>
</plugin>
</plugins>
</build>
而且官方还给出了单元测试代码覆盖率输出的配置:Activating coverage in a Maven project
其实现在而言用处不大,一般使用jacoco也可以在sonarqube配置。
@Mocked修饰类或者接口,类或者接口被修饰后,是全局的,以后这个类或者对象的方法调用就会走mocked的实例,不会再执行原来的方法。@Mocked非常霸道,自己new一个对象也不会生效了,全部被Mocked的实例接管。
返回结果jmockit也会处理:
测试一下,jmockit有两种注入方式
模拟单元测试验证正确,
Mocked注解要慎用,一旦Mocked,所有对象全部被Mocked,建议在方法的参数上使用注解,就像笔者上面的示例一样,这样只会接管当前方法有效,同理因为这个原因,使用Mocked可以支持静态方法mock。
Mocked注解会对所有范围内的类或者实例全部Mocked,如果只想Mocked当前实例,就需要@Injectable注解,由于仅仅Mocked当前实例,所有静态方法失效,new的对象失效。
@Test
void mocked(@Injectable User user){
assertThat(user.getFinalName("sss")).isNull();
assertThat(user.getResult("sss")).isNull();
assertThat(User.getStaticName("sss")).isNotNull();
assertThat(new User().getFinalName("sss")).isNotNull();
}
表示被测试的对象,如果我们没有实例化,JMockit会帮我们实例化。如果带有Injectable注解的属性,JMockit会使用Injectable构造函数实例化,如果没有这种构造函数,这会通过默认构造函数实例化,通过属性注入Injectable注解的属性。etg
public class SendMailService {
public String sendmail(){
return "OK";
}
}
@Test类
public class User {
private SendMailService sendMailService;
public String send(String str){
return str + sendMailService.sendmail();
}
}
@Capturing同样是mock对象,但与@Mocked或者@Injectable是有很大区别的。
@Capturing可以mock接口或者实现类的子类的行为。平时基本上不用,但是在AOP切面的过程可以直接mock接口或者类的子类行为。也就是说除了有@Mocked的能力,还会对动态代理或者自己写的子类mock。etg
public interface AopService {
String doAop(String param);
}
假设这是一个动态代理的接口:实现类N多,做了一个切面;或者这个接口没有实现,类似mybatis的mapper。这个时候@Capturing就派上用场了。可以模拟子类或者实现类的行为。
class AopServiceTest {
@Capturing
AopService aopService;
@Test
void doAop() {
new Expectations(){{
aopService.doAop(anyString);
result = "aopMock";
}};
assertThat(aopService.doAop("sss")).isEqualTo("aopMock");
}
}
expectations的录制其实有2种方式,可录制静态方法,final方法,但私有方法与native方法不可录制。
Invalid Class argument for partial mocking (use a MockUp instead)
笔者上一章报了这个错,推荐我们使用MockUp,静态方法,native,final方法,但私有方法不可录制,而且要写很多代码。
测试私有方法mock时,难道1.49版本不允许mock私有方法了?
java.lang.IllegalArgumentException: Unsupported fake for private method
看见有人回复想办法mock私有方法,源码分析是被作者禁了,至于是否有新的方式实现也没看见说明,官方API文档也没发现
非private方法是可以正常MockUp的。
@Test
void doAopMockUp() {
User user = new User();
new MockUp<User>(User.class){
@Mock
public String getResult(String test){
return "mock";
}
};
assertThat(user.getResult("111")).isEqualTo("mock");
}
这种方式很灵活,可以实现自定义部分mock,估计对公共类使用是很好的场景。
但是缺点很明显:
Verifications用于验证录制是否执行了,对比上一章的mockito,发现mock的3要素都是不变的。mockito是打桩-执行-验证,jmockit是录制-回放-验证。设计原理差不多,Verifications方面感觉jmockit就比较弱了。
@Test
void doAopMockUp() {
User user = new User();
new MockUp<User>(User.class){
@Mock
public String getResult(String test){
return "mock";
}
};
assertThat(user.getResult("111")).isEqualTo("mock");
new Verifications(){
{
user.getResult("111");
times = 1;
}
};
以Spring Boot为例,pom依赖如下
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.49</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}"/org/jmockit/jmockit/1.49/jmockit-1.49.jar
</argLine>
</configuration>
</plugin>
</plugins>
</build>
可以使用@Tested与@Injectable配合Spring的注解同时使用,使用上一章的示例
@SpringBootTest
class DemoServiceImplTest {
@Tested
@Autowired
private DemoService demoService;//这里就可以使用接口类型了,这点jmockit强大
@Test
void getMapperData(@Injectable DemoMapper demoMapper) {
new Expectations(){
{
demoMapper.getName(anyString);
result = "JMockit";
}
};
assertThat(demoService.getMapperData("sss")).isNotBlank().isEqualTo("JMockit");
}
}
从功能上讲jmockit是很强大的,但是笔者测试1.49版本无法mock私有方法,powerMock却是可以,但是powerMock不支持junit5。估计Spring Boot官方集成mockito而不是jmockit主要是: