一、痛点问题
1.1、如何mock静态方法
1.2、mock很容易造成代码覆盖率的下降,如何提升代码覆盖率
二、准备工作
2.1、技术选型:powerMock
因为Mockito使用继承的方式实现mock的,用CGLIB生成mock对象代替真实的对象进行执行,为了mock实例的方法,你可以在subclass中覆盖它,而static方法是不能被子类覆盖的,所以Mockito不能mock静态方法。
但PowerMock可以mock静态方法,因为它直接在bytecode上工作。PowerMock是一个JUnit扩展,它利用了EasyMock和Mockito模拟静态method的方法对Java中的静态method进行Mock。
2.2、maven依赖
<!-- mock 测试工具 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.4</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
三、项目实战
3.1、需求
对上游的数据做数据验证,如果验证成功则落库,如果验证失败则发告警邮件通知上游的所有收件人(邮件组)。现在单元测试时候不能真的给上游邮件组全体发邮件,需要mock功能,而且为了单元测试的case可以反复执行,上游数据不能真的落库。做到上述的话,还需要尽可能的提升代码覆盖率(公司的高质量战略的要求:代码的行覆盖率需要达到50%以上,这也是研发对自己代码质量的要求)。
3.2、代码实战
@QmqConsumer(prefix = CommissionOrderConstants.orderConfirmTopic, consumerGroup = CommissionConstants.appId)
public void onMessage(Message message) {
log.info("receive commission order confirm's message.", message);
long startTime = System.currentTimeMillis();
try {
String data = message.getStringProperty("data");
QmqCommissionOrderConfirmVo commissionOrderConfirmVo = JSON.parseObject(data, QmqCommissionOrderConfirmVo.class);
this.handle(commissionOrderConfirmVo);
} catch (SQLException e) {
log.error("receive bi commission order confirm's message error!", e);
message.ack(System.currentTimeMillis() - startTime, e);
}
}
private void handle(QmqCommissionOrderConfirmVo vo) throws SQLException {
boolean checkResult = check(vo);
if (checkResult) {
// 数据落地
insertDate(vo);
} else {
log.error("BI订单数据校验不通过。{}", JSON.toJSONString(vo));
alertByEmail(vo);
}
}
private boolean check(QmqCommissionOrderConfirmVo vo) throws SQLException {
// 一系列的字段校验等逻辑
if(xxxx) {
return true;
} else {
return
}
}
// 发告警邮件
private void alertByEmail(QmqCommissionOrderConfirmVo vo) {
BiOrderAlertEmailConfig biOrderAlertEmailConfig = serviceConfig.getBiOrderAlertEmailConfig();
//邮件内容(html)
String emailBodyHtml = "订单数据推送发生异常。" + vo;
String subject = "";
if (Foundation.server().getEnv() == Env.PRO) {
subject += "[PROD]" + biOrderAlertEmailConfig.getSubject();
} else if(Foundation.server().getEnv() == Env.UAT) {
subject += "[UAT]" + biOrderAlertEmailConfig.getSubject();
} else {
subject += "[FAT]" + biOrderAlertEmailConfig.getSubject();
}
SendEmailVo sendEmailVo = new SendEmailVo(biOrderAlertEmailConfig.getRecipientList(),
biOrderAlertEmailConfig.getCopierEmail(), emailBodyHtml, subject);
EmailUtils.sendHtmlEmail(sendEmailVo);
}
告警邮件的工具类如下:
@Slf4j
public class EmailUtils {
private static final String SENDER = "xxx@xxxx.com";
private static final String SEND_CODE = "12010037";
private static final String CHARSET = "UTF-8";
private static final Integer BODY_TEMPLATE_ID = 12010037;
private static final int RETRY_MAX_TIMES = 5;
private static final int RETRY_INITIAL_TIMES = 1;
// 第三方邮件服务的客户端
private static EmailServiceClient client = EmailServiceClient.getInstance();
/**
* 发送邮件(未带附件)
*
* @param sendEmailVo
* @return
*/
public static SendEmailResponse sendHtmlEmail(SendEmailVo sendEmailVo) {
SendEmailResponse emailResponse = sendHtmlEmail(sendEmailVo, null);
return emailResponse;
}
/**
* 发送带附件的邮件
*
* @param sendEmailVo
* @param attachments
* @return
*/
public static SendEmailResponse sendHtmlEmail(SendEmailVo sendEmailVo, List<Attachment> attachments) {
List<Attachment> attachmentList = doUploadAttachment(attachments);
SendEmailResponse emailResponse = doSendHtmlEmail(sendEmailVo, attachmentList);
return emailResponse;
}
private static SendEmailResponse doSendHtmlEmail(SendEmailVo sendEmailVo, List<Attachment> attachmentList) {
SendEmailRequest sendEmailRequest = new SendEmailRequest();
sendEmailRequest.setAppID(Integer.valueOf(CommissionConstants.appId));
sendEmailRequest.setSender(SENDER);
sendEmailRequest.setSendCode(SEND_CODE);
sendEmailRequest.setCharset(CHARSET);
sendEmailRequest.setBodyTemplateID(BODY_TEMPLATE_ID);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR, 1);
sendEmailRequest.setExpiredTime(calendar);
// 收件人邮箱地址
sendEmailRequest.setRecipient(sendEmailVo.getRecipientList());
// 抄送人邮箱地址
if (CollectionUtils.isNotEmpty(sendEmailVo.getCopierEmail())) {
sendEmailRequest.setCc(sendEmailVo.getCopierEmail());
}
sendEmailRequest.setSubject(sendEmailVo.getSubject());
// 邮件体
sendEmailRequest.setBodyContent(String.format("<entry><content><![CDATA[%s]]></content></entry>", sendEmailVo.getEmailBodyHtml()));
sendEmailRequest.setIsBodyHtml(true);
// 添加附件
if (CollectionUtils.isNotEmpty(attachmentList)) {
sendEmailRequest.setAttachmentList(attachmentList);
}
// 尝试多次发送邮件,若不成功,则报错
for (int i = RETRY_INITIAL_TIMES; i <= RETRY_MAX_TIMES; i++) {
try {
SendEmailResponse response = client.sendEmail(sendEmailRequest);
if (response != null && EmailEnum.SEND_EMAIL_SUCCESS.getCode() == response.getResultCode()) {
return response;
}
} catch (Exception e) {
log.error("Send Email Failed", e);
if (i == RETRY_MAX_TIMES) {
throw new EmailSendFailedException(e);
}
}
}
return null;
}
private static List<Attachment> doUploadAttachment(List<Attachment> attachments) {
if (CollectionUtils.isNotEmpty(attachments)) {
UploadAttachmentRequest request = new UploadAttachmentRequest();
request.setAttachmentList(attachments);
// 尝试多次上传附件,若不成功,则报错
for (int i = RETRY_INITIAL_TIMES; i <= RETRY_MAX_TIMES; i++) {
try {
UploadAttachmentResponse response = client.uploadAttachment(request);
if (response != null && EmailEnum.UPLOAD_ATTACHMENT_SUCCESS.getCode() == response.getResultCode()) {
List<Attachment> attachmentList = response.getAttachmentList();
return attachmentList;
}
} catch (Exception e) {
log.error("upload attachment error", e);
if (i == RETRY_MAX_TIMES) {
throw new EmailSendFailedException(e);
}
}
}
}
return null;
}
}
mock单元测试的代码如下:
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
// 跟spring不兼容,需要去除Spring框架的安全性验证。本例中因为用到了soa的远程调用,所以还需要ignore掉"javax.crypto.*", "com.sun.management.*"等不重要的限制
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*", "javax.crypto.*", "com.sun.management.*"})
@PrepareForTest({EmailUtils.class, EmailServiceClient.class})
@SpringBootTest
public class ConsumerBiDateTest {
@Autowired
private MessageProducerProvider producer;
@Autowired
private BiOrderLandListener biOrderLandListener;
@MockBean
private EmailServiceClient mockClient;
@Autowired
private BiOrderConfirmListener biOrderConfirmListener;
@MockBean
private CommissionOrdersDao commissionOrdersDao;
@Test
public void testBiConfirmDate() throws Exception {
// 不要真的数据落地,也不要真的给上游发告警邮件
mockDao();
mockEmail();
String data = "{\"vDate\":\"2019-11-01\",\"count\":1}";
String topic = "ibu.bi.to.plt.commission.order.confirm";
Message message = producer.generateMessage(topic, 24, TimeUnit.HOURS);
message.setProperty("type", "commissionOrderConfirm");
message.setProperty("version", System.currentTimeMillis());
message.setProperty("data", data);
biOrderConfirmListener.onMessage(message);
}
private void mockEmail() throws Exception {
SendEmailResponse sendEmailResponse = new SendEmailResponse();
sendEmailResponse.setResultCode(EmailEnum.SEND_EMAIL_SUCCESS.getCode());
sendEmailResponse.setResultMsg("mock send email success!");
// Notice:下面两种mock方式都可以,看你自己怎么选择,方式2的代码覆盖率更高(更有方案,被mock的代码行更少)
if (1 == 2) {
// 方式1
PowerMockito.mockStatic(EmailUtils.class);
PowerMockito.when(EmailUtils.sendHtmlEmail(any())).thenReturn(sendEmailResponse);
} else {
// 方式2
PowerMockito.mockStatic(EmailServiceClient.class);
when(mockClient.sendEmail(any())).thenReturn(sendEmailResponse);
PowerMockito.when(EmailServiceClient.getInstance()).thenReturn(mockClient);
}
}
private void mockDao() {
when(commissionOrdersDao.insert(any())).thenReturn(null);
}
}
四、局限性
PowerMock底层使用了自带的classLoader去加载加载类,所以才能很强大的mock各种静态方法。但是这也很可能会导致和一些框架冲突。
4.1、@MockBean会导致测试类之间springboot application context
不断启动多次!Debug了一下, 如果运行powermock 注解的类, protocol 使用的classLoader 是MockClassLoader(MockClassLoader的parent是AppClassLoader), 而 SpringBootTest 注解的类,protocol使用的是AppClassLoader。
核心断点:private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
Protocol 和 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension() 是由于使用了不同的classloader 才会报java.lang.ClassCastException: com.alibaba.dubbo.rpc.Protocol$Adaptive cannot be cast to com.alibaba.dubbo.rpc.Protocol的错
我在我的项目中涉及到了Qschedule的定时任务框架,但是qschedule 不允许application context启动多次,会报duplicate 错误。
解决方案:
因为我一定要使用定时任务,不愿意换一个定时任务框架,所以只能在PowerMock上想办法。
一种是绕过maven clean install命令时候的扫描执行测试类(更改类名不再以Test开头和结尾,也不以TestCase结尾,让maven扫描不到),但是治标不治本,代码覆盖率会下降。
第二种是舍弃PowerMock,不再去mock静态方法,也算是无奈之举吧。
总结:
powerMock虽然现在还不能完全完美的兼容到spring,但是也是可以使用PowerMockIgnore来绕过一些安全性的验证。而且powerMock确实很强大,系mockito的班底,正在茁壮成长,还是推荐大家多多使用。