声明:
Author:赵志乾
Date:2017-8-5
Declaration:All Right Reserved!!!
1、本系列教程主要是介绍JMockit库中一些注解和API的使用,这些注解和API能够简化测试用例的编写。首先是能够使被测对象自动实例化和初始化的注解;之后是模拟依赖的ExpectationsAPI;最后是仿造应用的mockupAPI(简化外部组件的模拟)。
注:该教程只是对JMockit库中一些常用的注解、API、类进行简单实例,详细内容请参考API文档手册。
2、开发人员通常需要自己借助一些测试框架(如JUnit)来写一些验证自己代码逻辑的测试用例。而自动化测试由可分为两类:一类是对从系统中分割出来的类或者组件进行测试的单元测试,另一类是对系统操作进行测试的整体测试(一次涉及多个单元)。
整体测试会涉及到多个组件或单元的交互,而通常在测试过程中我们感兴趣的并不是所有的单元、组件或子系统,此时就需要对那些不感兴趣的组件进行模拟或仿制,从而从系统中分离出我们感兴趣的代码进行测试。因此,能够“将代码从系统中进行分离”这种特性非常有用,但是,在实际使用中,我们要尽量保证测试环境的真实性,也就是说在理想情况下,应该使模拟或者仿制的部分压缩到最小。
3、通常而言,一个mock出来的对象是在单一测试或多个相关测试过程中对某个类的特定实现,它会在测试过程取代依赖对象,并且表现出预期的行为。但是这并不是mock对象唯一的能力,因为mock对象本身也可进行一些断言。JMockit优于传统mock对象的方面是:其支持在非模拟类上直接mock方法和构造函数,避免了测试过程中对mock对象进行实例化的需求。并且在测试过程,真实类上被模拟的方法和构造函数何时被调用,其都会表现出我们预定义的行为。而当一个类被模拟的时候,测试期间,模拟代码会取代原有代码。这些模拟方式不仅适用于实例方法,也适用于静态方法、final方法和构造函数。
注:mcok不仅适用于单元测试,也适用于整体测试。
4、示例演示:
假定我们有一个业务服务类,其遵循以下步骤来完成业务操作:
取出某个持久化的实体;
将实体的新状态进行持久化;
向某个需要感知该操作的组件发送邮件进行告知;
前两个步骤需要访问应用的数据库,这通常会使用持久化子系统的简化API来做,这里使用的JPA。而最后一步需要使用第三方的API来发送邮件(示例中使用的是Apache的 Commons Email库 )。可以看出,这个服务类有两个依赖,一个用于数据持久化、另一个用于发送邮件。为了测试业务操作,我们会使用JMockit的JPA来处理持久化操作,而发送邮件则使用模拟的API来完成。被测类的源代码如下:
package tutorial.domain; import java.math.*; import java.util.*; import org.apache.commons.mail.*; import static tutorial.persistence.Database.*; public final class MyBusinessService { private final EntityX data; public MyBusinessService(EntityX data) { this.data = data; } public void doBusinessOperationXyz() throws EmailException { List<EntityX> items = (1) find("select item from EntityX item where item.someProperty = ?1", data.getSomeProperty()); // Compute or obtain from another service a total value for the new persistent entity: BigDecimal total = ... data.setTotal(total); (2) persist(data); sendNotificationEmail(items); } private void sendNotificationEmail(List<EntityX> items) throws EmailException { Email email = new SimpleEmail(); email.setSubject("Notification about processing of ..."); (3) email.addTo(data.getCustomerEmail()); // Other e-mail parameters, such as the host name of the mail server, have defaults defined // through external configuration. String message = buildNotificationMessage(items); email.setMsg(message); (4) email.send(); } private String buildNotificationMessage(List<EntityX> items) { ... } }
数据库类只包含一些静态方法和一个私有的构造函数,而find和persist方法的意义很明确,此处将不再进行说明。那么接下来我们要考虑如何在不修改原有代码的情况下来测试doBusinessOperationXyz这个方法?下面是JUnit的测试类,其中测试将会验证持久化操作执行情况以及发送邮件API的调用情况。这些验证点位于上面示例代码中的数字标识位置。
package tutorial.domain; import org.apache.commons.mail.*; import static tutorial.persistence.Database.*; import org.junit.*; import org.junit.rules.*; import static org.junit.Assert.*; import mockit.*; public final class MyBusinessServiceTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @Tested final EntityX data = new EntityX(1, "abc", "someone@somewhere.com"); @Tested(fullyInitialized = true) MyBusinessService businessService; @Mocked SimpleEmail anyEmail; @Test public void doBusinessOperationXyz() throws Exception { EntityX existingItem = new EntityX(1, "AX5", "abc@xpta.net"); (1) persist(existingItem); businessService.doBusinessOperationXyz(); (2) assertNotEquals(0, data.getId()); // implies "data" was persisted (4) new Verifications() {{ anyEmail.send(); times = 1; }}; } @Test public void doBusinessOperationXyzWithInvalidEmailAddress() throws Exception { final String email = "invalid address"; data.setCustomerEmail(email); (3) new Expectations() {{ anyEmail.addTo(email); result = new EmailException(); }}; thrown.expect(EmailException.class); businessService.doBusinessOperationXyz(); } }
上面的示例中使用了JMockit的一些注解:其中@Tested注解会正确的初始化被测对象,@Mocked注解则用于模拟一个给定的类型。
在测试类示例中,expectations的录制(位于new Expectations() {{ ... }}中)和验证(位于new Verifications() {{ ... }}中)在模拟类型或者实例上的对应方法被调用时会起作用。其中,对于返回值或异常抛出都是通过字段result进行指定,而调用次数则通过字段times进行指定。
注:本博客中的实例代码均来自于JMockit官方教程。