一、痛点问题

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的错

java怎么mock静态方法 mock静态类方法_java怎么mock静态方法


java怎么mock静态方法 mock静态类方法_powerMock_02

 

我在我的项目中涉及到了Qschedule的定时任务框架,但是qschedule 不允许application context启动多次,会报duplicate 错误。

解决方案:

因为我一定要使用定时任务,不愿意换一个定时任务框架,所以只能在PowerMock上想办法。

一种是绕过maven clean install命令时候的扫描执行测试类(更改类名不再以Test开头和结尾,也不以TestCase结尾,让maven扫描不到),但是治标不治本,代码覆盖率会下降。

第二种是舍弃PowerMock,不再去mock静态方法,也算是无奈之举吧。

总结:

powerMock虽然现在还不能完全完美的兼容到spring,但是也是可以使用PowerMockIgnore来绕过一些安全性的验证。而且powerMock确实很强大,系mockito的班底,正在茁壮成长,还是推荐大家多多使用。