单元测试系列:Mock工具Jmockit使用介绍

博客园发布地址:http://www.cnblogs.com/zishi/p/6760272.html

Mock工具Jmockit使用介绍

在写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖的类或者资源又会有依赖,导致在单元测试代码里无法完成构建,我们应对的方法是Mock。简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。

1.1 mock工具列表

可用的Mock Toolkit有许多,比较常见的有EasyMock, Jmock和JMockit等等,到底选哪个呢,Jmockit的官网上有个特性对比列表,很详细,他们的功能对比如下:

http://www.cnblogs.com/zishi/p/6760272.html

(注:因为原图比较大,所以建议到网页版中查看)

1.2 Mockito简介

EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这两种 Mock 工具都不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,但是这些方法往往是我们在大型系统中需要的功能。

关于更多Mockito2.0新特性,参考官方介绍文档,里边有关于为什么不mock private的原因,挺有意思的:

https://github.com/mockito/mockito/wiki/What%27s-new-in-Mockito-2

Mockito 的使用

###Maven###

通过Maven管理的,需要在项目的Pom.xml中增加如下的依赖:

org.mockitomockito-all1.9.0test

在程序中可以import org.mockito.Mockito,然后调用它的static方法。

1.2.1 模拟对象

创建 Mock 对象的语法为 mock(class or interface)。

1.2.2 设置对象调用的预期返回值

通过 when(mock.someMethod()).thenReturn(value) 来设定 Mock 对象某个方法调用时的返回值。或者使用 when(mock.someMethod()).thenThrow(new RuntimeException) 的方式来设定当调用某个方法时抛出的异常。

1.2.3 验证被测试类方法

Mock 对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择的对它的 交互行为进行验证。在 Mockito 中验证 Mock 对象交互行为的方法是 verify(mock).someMethod(…)。最后 Assert() 验证返回值是否和预期一样。

1.2.4 Demo

Mock 对象的创建

mock(Class classToMock); mock(Class classToMock, String name) mock(Class classToMock, Answer defaultAnswer) mock(Class classToMock, MockSettings mockSettings) mock(Class classToMock, ReturnValues returnValues)

可以对类和接口进行mock对象的创建,创建时可以为mock对象命名。对mock对象命名的好处是调试的时候容易辨认mock对象。

Mock对象的期望行为和返回值设定

假设我们创建了LinkedList类的mock对象:

LinkedList mockedList = mock(LinkedList.class);1.3 PowerMock简介

PowerMock 是在 EasyMock 以及 Mockito 基础上的扩展,通过定制类加载器等技术,PowerMock 实现了之前提到的所有模拟功能,使其成为大型系统上单元测试中的必备工具。缺点是缺少文档。

mock是模拟对象,用于模拟真实对象的行为。

Powermock主要用于打桩。比如:方法A的参数需要传入实例B,方法A需要调用B的某个方法B.C()。方法C因为耗时长或者根本没有实现或者其他不方便在单元测试中实现等原因,需要伪造返回,此时Powermock即可派上用场。

PowerMock扩展了EasyMock和Mockito框架,增加了对static和final方法mock支持等功能。这里主要基于PowerMock Mockito API进行介绍。

PowerMock支持JUnit和TestNG,这里基于JUnit。

安装

下载地址:https://github.com/jayway/powermock/wiki/Downloads。下载" Mockito and JUnit including dependencies"版本。当前版本为”powermock-mockito-junit-1.6.3.zip"。

1.4 Stub和Mock

###Mock###

所谓的mock,即模拟,模仿的意思。Mock 技术的主要作用是使用mock工具模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试目标与测试边界以外的对象隔离开。

###Stub###

Stub,桩。单元测试过程中,对于在应用中不容易构造或者比较复杂的对象,用一个虚拟的对象来代替它。从类的实现方式上看,stub有一个显式的类实现,按照stub类的复用层次可以实现为普通类(被多个测试案例复用),内部类(被同一个测试案例的多个测试方法复用)乃至内部匿名类(只用于当前测试方法)。stub的方法也会有具体的实现,哪怕简单到只有一个简单的return语句。

###Stub 与 Mock 的区别###

Stub 是在单元测试过程中去代替某些对象来提供所需的测试数据,适用于基于状态的(state-based)测试,关注的是输入和输出。而Mock适用于基于交互的(interaction-based)测试,关注的是交互过程,不只是模拟状态,还能够模拟模块或对象的行为逻辑并能验证其正确性,Mock不需要类的显示实现,直接用工具模拟。

综合考量下来,所以我们的mock工具也选择了jmockit(http://jmockit.org/index.html)

关于如何使用:

推荐:在Maven 的pom.xml文件中添加以下依赖节点:

org.jmockitjmockit1.30test

当然也可以在项目中直接引入jar包。

JMockit模拟API可用于JUnit 4(版本4.5或更高版本),JUnit 5或TestNG(版本6.2或更高版本)编写测试。 现在让我们看看这个API是如何进行模拟的,为了便于理解,下面引用官网给出的范例代码,同时我们将必要的类库进行引用。

在测试类中,声明一个你想要模拟的类型的mock字段,并用@Mocked,@Injectable或@Capturing注释。当模拟类时,@Injectable意味着只有被分配mock字段的实例将具有mock行为; 否则,被mock的类的所有实例将被mock。

importorg.junit.*;importmockit.*;publicclassMyFirstJMockitTest{//Mocked实例(而不是常规的“mock对象”)将自动创建并分配到带注释的mock字段@Mocked Collaborator mock1;//所有当前和未来的实例都会被mock@Injectable AnotherDependency anotherMock;//只有一个特定实例被mock@TestpublicvoidmyFirstTestMethod() {//任何mock字段都可以在这里或者类的任何其他测试方法中使用} @TestpublicvoidtestMethodWithMockParameter(@Mocked YetAnotherDependency testSpecificMock) { ... } ...}

注意:上面的测试类显示了一些不同的东西:第二个测试方法声明一个参数! 通常,JUnit / TestNG测试方法不允许有参数。 然而,当使用JMockit时,允许这样的模拟参数。 一般来说,只有测试类中大多数或所有测试都需要Mock类型时,才使用测试类的Mock字段。 否则,Mock的范围最好仅限于单个测试的Mock参数。 JMockit将总是关注实例化Mock类型,并且当测试运行器调用测试方法时,将实例分配给mock字段(假设字段不是final)或将其作为参数传递。

要Mock测试的方法如下:

publicclassMyObject {publicString hello(String name){return"Hello " +name; }}

使用JMockit编写的单元测试如下:

@Mocked//用@Mocked标注的对象,不需要赋值,jmockit自动mockMyObject obj;@TestpublicvoidtestHello() {newNonStrictExpectations() {//录制预期模拟行为{ obj.hello("Zhangsan"); returns("Hello Zhangsan");//也可以使用:result = "Hello Zhangsan";} }; assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//调用测试方法newVerifications() {//验证预期Mock行为被调用{ obj.hello("Hello Zhangsan"); times= 1; } };}

代码完成后,运行单元测试,结果如下:

JMockit也可以分类为非局部模拟与局部模拟,区分在于Expectations块是否有参数,有参数的是局部模拟,反之是非局部模拟。

而Expectations块一般由Expectations类和NonStrictExpectations类定义,类似于EasyMock和PowerMock中的Strict Mock和一般性Mock。

用Expectations类定义的,则mock对象在运行时只能按照 Expectations块中定义的顺序依次调用方法,不能多调用也不能少调用,所以可以省略掉Verifications块;

而用NonStrictExpectations类定义的,则没有这些限制,所以如果需要验证,则要添加Verifications块。

上述的例子使用了非局部模拟,下面我们使用局部模拟来改写上面的测试,代码如下:

@TestpublicvoidtestHello() {finalMyObject obj =newMyObject();newNonStrictExpectations(obj) {//录制预期模拟行为{ obj.hello("Zhangsan"); returns("Hello Zhangsan");//也可以使用:result = "Hello Zhangsan";} }; assertEquals("Hello Zhangsan", obj.hello("Zhangsan"));//调用测试方法newVerifications() {//验证预期Mock行为被调用{ obj.hello("Hello Zhangsan"); times= 1; } };}

模拟静态方法:

@TestpublicvoidtestMockStaticMethod() {newNonStrictExpectations(ClassMocked.class) { { ClassMocked.getDouble(1);//也可以使用参数匹配:ClassMocked.getDouble(anyDouble);result = 3; } }; assertEquals(3, ClassMocked.getDouble(1));newVerifications() { { ClassMocked.getDouble(1); times= 1; } };}

模拟私有方法:

如果ClassMocked类中的getTripleString(int)方法指定调用一个私有的multiply3(int)的方法,我们可以使用如下方式来Mock:

@TestpublicvoidtestMockPrivateMethod()throwsException {finalClassMocked obj =newClassMocked();newNonStrictExpectations(obj) { {this.invoke(obj, "multiply3", 1);//如果私有方法是静态的,可以使用:this.invoke(null, "multiply3")result = 4; } }; String actual= obj.getTripleString(1); assertEquals("4", actual);newVerifications() { {this.invoke(obj, "multiply3", 1); times= 1; } };}

接下来我们用Jmockit实现一个具体的单元测试,首先下面是一段Controller的功能代码:

importcom.odde.mail.model.Result;importcom.odde.mail.service.MailService;importorg.apache.commons.logging.Log;importorg.apache.commons.logging.LogFactory;importorg.codehaus.jackson.map.ObjectMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.ResponseBody;importstaticjava.lang.String.format;@Controller@RequestMapping("/mail")publicclassMailController {privatestaticfinalLog log = LogFactory.getLog(MailController.class);privatefinalObjectMapper mapper =newObjectMapper(); @AutowiredprivateMailService mailService; @RequestMapping(value= "/send", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")public@ResponseBody String send(@RequestParam("recipients") String recipients, @RequestParam("subject") String subject, @RequestParam("content") String content)throwsException { log.debug("mail controller send start"); log.debug(format("recipients:%s", recipients)); log.debug(format("subject:%s", subject)); log.debug(format("content:%s", content)); Result mailResult=mailService.send(recipients, subject, content); String result=mapper.writeValueAsString(mailResult); log.debug(format("result:%s", result)); log.debug("mail controller send finish");returnresult; }}

接下来我们看一下Jmockit实现的具体的单元测试代码:

importcom.odde.mail.model.Result;importcom.odde.mail.service.MailService;importmockit.Expectations;importmockit.Injectable;importmockit.Tested;importmockit.integration.junit4.JMockit;importorg.junit.Test;importorg.junit.runner.RunWith;importstaticorg.hamcrest.CoreMatchers.is;importstaticorg.junit.Assert.assertThat;@RunWith(JMockit.class)publicclassMailControllerTest { @Tested MailController mailController; @InjectableprivateMailService mailService; @Testpublicvoidshould_return_status_success_when_send_mail_success()throwsException {newExpectations() { { mailService.send("test@test.com", "test", "test"); result=newResult("成功"); } }; String result= mailController.send("test@test.com", "test", "test"); assertThat(result, is("{\"status\":\"成功\"}")); }}

@RunWith(JMockit.class): 指定单元测试的执行类为JMockit.class;

@Tested: 这个是指被测试类,在这个测试案例中我们要测试的是MailController,所以我们给其打上这个标签;

@Injectable: 这个可以将对象进行mock并自动关联到被测试类,而不需要通过其他文件类似spring的配置文件等来进行关联;

@Expectations: mock对象mailService的send方法,让其返回一个Result对象;

做完上面这些基本就可以了,后面的被测方法调用和验证都跟原来的一样。这样看起来是不是比原来的单元测试代码少了一些,也更简洁了一些,最重要的一点是这样的单元测试不依赖spring的bean定义文件,不需要启动web服务,执行起来速度很快。

首先仍然是先看一下Service的功能代码,代码也比较简单,就是调用Repository做一些增删改查的动作。

importcom.odde.mail.model.Recipient;importcom.odde.mail.model.Result;importcom.odde.mail.repo.RecipientRepository;importorg.apache.commons.logging.Log;importorg.apache.commons.logging.LogFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importjava.util.List;@ServicepublicclassRecipientService { @AutowiredprivateRecipientRepository recipientRepository;publicResult add(String username, String email) { Recipient recipient=recipientRepository.findByEmail(email); Result result;if(recipient ==null) { recipientRepository.save(newRecipient(username, email)); result=newResult("成功"); }else{ result=newResult("失败"); }returnresult; }}

接着就是它的单元测试代码,我们看一下Jmockit如何实现的mock代码:

importcom.odde.mail.model.Recipient;importcom.odde.mail.model.Result;importcom.odde.mail.repo.RecipientRepository;importmockit.Injectable;importmockit.NonStrictExpectations;importmockit.Tested;importmockit.integration.junit4.JMockit;importorg.junit.Test;importorg.junit.runner.RunWith;importjava.util.List;importstaticjava.util.Arrays.asList;importstaticorg.hamcrest.core.Is.is;importstaticorg.junit.Assert.assertThat;@RunWith(JMockit.class)publicclassRecipientServiceTest { @TestedprivateRecipientService recipientService; @InjectableprivateRecipientRepository recipientRepository; @Testpublicvoidshould_return_success_when_add_recipient_not_exist()throwsException { Result result= recipientService.add("Tom", "test@test.com"); assertThat(result.getStatus(), is("成功")); }}

相对Controller Test这里少了一步对recipientRepository对象findByEmail方法的mock,因为如果不通过Expectations进行方法mock的话,方法会默认返回null,而我们要测试的场景正是需要findByEmail方法返回null,所以mock方法这一步我们也省了。改写后的整体代码也比原来的少了很多,而且速度更快。

JMockit功能非常强大,不仅可以轻松处理上面的这些测试场景,还可以对static,final,private等方法进行mock,可以让你的单元测试毫无阻碍的进行。

但是如果过度的使用Mock框架,会让功能代码的真正问题被掩盖。本来单元测试的设计可以让你发现功能代码上的一些设计是否合理,比如有没有紧耦合等,但使用JMockit可以让你在设计不合理的代码上也可以轻松地进行单元测试,这样你就很难发现功能代码上的问题了。

所以建议JMockit等类似的mock框架还是要谨慎使用,首先要保证功能代码设计合理,满足面向对象设计的要求,再来考虑提高单元测试效率的问题。

另外,Mock的函数是不计算到单元测试覆盖率里边的,如下图所示:

注意:我们mock了hello方法,但是在EclEmma中显示覆盖率为0%。

问题1 编译报错方法名无法找到

在pom.xml文件中,注意依赖次序,JMockit一定要在JUnit之前,否则容易出现编译报错方法名找不到之类的奇葩问题:

问题2 报错Jmockit初始化异常

引用的版本必须保持一致,可以在build path里查看是否是自己用的版本,否则会报错Jmockit初始化异常:

问题3 版本造成java.lang.NoSuchMethodError

Junit使用最新版本是4.12。系统默认的junit版本太低了。会报异常如下:

java.lang.NoSuchMethodError: org.junit.runner.Request.classWithoutSuiteMethod(Ljava/lang/Class;)Lorg/junit/runner/Request;

at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createFilteredTest(JUnit4TestLoader.java:76)

at org.eclipse.jdt.internal.

改为最新版本,Junit运行正常:

junitjunit4.12问题4 报错cannot be resolved to a type

调试过程中报错:

xxx cannot be resolved to a type

解决方法,在发生错误的项目上单击鼠标右键-〉Properties,选中“Resource”,右侧Text file encoding选择“Other:UTF-8”,点击“Apply”按钮。

附录:参考文档一览

JMockit官网:http://jmockit.org/

感谢阅读,作者原创技术文章,转载请注明出处

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180321G0U9KS00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励