springboot集成mockito与powermock
mockito大家都比较熟悉了,存在或者不存在,都不要紧,mockito让你有一种只要一出手,就知道有没有的感觉。但是它也不是万能的,比如静态方法、私有方法,它就无能为力了。这是为什么呢?当然不是mockito的框架或现有技术解决不了,而是出于某些原因或立场,比如测试理念观点。甚至在mockito的FAQ中,作者明确了每一项未实现的功能不支持的原因,或者干脆说已经有别的工具实现了,需要的话,去用那个工具吧,我不愿意重复造轮子。
当然实现这些也并非轻而意举,比如如何mock final类,特别是jdk中的final类,比如String。但作为系统类,在任何时候都不应该可以被修改(即使是有办法修改,也不建议去修改,也没有必要修改,否则重新设计一门新语言即可),特别是对于java.lang包下的类,如基本的数据类型Integer、Long等。java agent可以修改由AppClassLoader加载的类,而endorsed技术也只允许覆盖在有限的限制列表中的类。而powermock采取的方案是,如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。
mockito的实现原理是用asm给需要mock的对象生成对应的代理对象,然后使用mock出来的对象即可。而在spring框架中,SpyBean与MockBean的原理也是一样,只不过还需要多做一步,就是用mock后的对象替换容器中原有的对象。
尽管如此,但mockito仍然有一些限制,罗列及翻译如下:
Mockito 2.x specific limitations
Mockito 2.x限制
Requires Java 6+
需JDK 6以上版本
Cannot mock static methods
不支持mock静态方法
Cannot mock constructors
不支持mock构造函数
Cannot mock equals(), hashCode(). Firstly, you should not mock those methods. Secondly, Mockito defines and depends upon a specific implementation of these methods. Redefining them might break Mockito.
Mocking is only possible on VMs that are supported by Objenesis. Don't worry, most VMs should work just fine.
不支持mock equals与hashCode方法。mockito认为不应该mock这两个方法,因为mockito的实现方案依赖于这两个方法。重新定义这两个方法可以会导致mockito异常。同时mocking只能在被Objenesis支持的vm上运行,目前在大部分vm上都能运行得很好。(Objenesis是一个使用旁门左道创建类实例的库,除了调用类的构造函数)
Spying on real methods where real implementation references outer Class via OuterClass.this is impossible. Don't worry, this is extremely rare case.
不支持当内部类的某个方法实现中引用外部OuterClass.this的情形。但不需要担心,这样的例子真的很少见。
Can I mock static methods?
支持mock静态方法吗
No. Mockito prefers object orientation and dependency injection over static, procedural code that is hard to understand & change. If you deal with scary legacy code you can use JMockit or Powermock to mock static methods.
不支持,Mockito更倾向于在面向对象与依赖注入的层面上mock,而不是mock静态方法,静态方法这种面向过程的代码比较难理解与改变。如果你要处理这些恐怖的遗留代码,那么请使用JMockit或者Powermock来mock静态方法。
Can I mock private methods?
支持mock私有方法吗
No. From the standpoint of testing... private methods don't exist. More about private methods here.
不支持。从测试方法的观点来看,其实私有方法是不存在的(需要测试的都是公开的方法)。更多关于私有方法的观点讲参照这里(注:原文这里是个链接)。
Why Mockito doesn't mock private methods?
为什么Mockito不支持mock私有方法
Firstly, we are not dogmatic about mocking private methods. We just don't care about private methods because from the standpoint of testing, private methods don't exist. Here are a couple of reasons Mockito doesn't mock private methods:
首先,关于mock私有方法论断,不是我们自以为是。我们不关注私有方法是因为测试方法相关的观点,私有方法是不存在的。这里有一些关于Mockito不支持mock私有方法的原因:
1. It requires hacking of classloaders that is never bullet proof and it changes the API (you must use custom test runner, annotate the class, etc.).
mock私有方法需要侵入classloader,虽然classloader并非刀枪不入,但是这样会改变你使用Mockito API的方式(比如使用自定义的test runner,使用特殊的注解等)
翻译加注:因为私有方法不可见,无法使用原有的mockito语法实现mock,所以必然需要其他途径来达到目的,而powermock使用自定义的classloader与runner来实现。静态方法的mock也是同样的原因,因为没有对象可以被mock,只能通过classloader做文章,自定义的classloader想返回什么字节码就返回什么字节码。
2.It is very easy to work around - just change the visibility of method from private to package-protected (or protected).
其实mock私有方法可以很简易地解决,只需要改变方法的可见性,如将private改为default或protected。
3. It requires the team to spend time implementing & maintaining it. And it does not make sense given point (2) and a fact that it is already implemented in different tool (powermock).
它需要Mockito团队花时间去实现并且维护它,但是基于观点2这又讲不过去(即说不服自己),并且事实上已经有不同的工具(powermock)实现它了。
4. Finally... Mocking private methods is a hint that there is something wrong with Object Oriented understanding. In OO you want objects (or roles) to collaborate, not methods. Forget about pascal & procedural code. Think in objects.
最后,对私有方法进行mock意味着对面向对象编程的概念理解有偏差。在面向对象的理念中你需要的是对象(或角色)来协作,而不是方法。忘记pascal,忘记面向过程编程吧,以面向对象的方式来思考。
public class MyUtil {
public static String hello() {
return "hello";
}
}
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
private String getName2() {
return this.name;
}
public String getName() {
return getName2();
}
}
package com.kidshelloworld.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
package com.kidshelloworld.test;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
@RunWith(PowerMockRunner.class)
@PowerMockIgnore(value = { "javax.management.*", "javax.net.ssl.*", "javax.net.SocketFactory" })
@ActiveProfiles(value = { "testcase" })
@PrepareForTest(value = { MyUtil.class, Person.class })
@SpringBootTest(classes = TestApplication.class)
public class TestMyUtil {
private Person spyPerson;
@autowired
public void prepare() {
PowerMockito.mockStatic(MyUtil.class);
Mockito.when(MyUtil.hello()).thenReturn("hi");
spyPerson = PowerMockito.spy(new Person("xyz");
}
@Test
public void testHello() {
Assert.assertEquals("hi", MyUtil.hello());
}
@Test
public void testGetName() {
PowerMockito.when(spy, "getName2").thenReturn("abc");
String name = spy.getName();
Assert.assertEquals("abc", name);
}
}
powermock使用了自定义的classloader来解决mock静态方法与私有方法的问题,因此其会为加了PrepareForTest注解的类生成对应的classloader来加载用到的类,这样就可能会导致其与系统的classloader加载了相同的类,导致类型转换失败,PowerMockIgnore注解则是告诉powermock放弃加载指定的这些类。
同时powermock使用了自定义的PowerMockRunner,与spring集成时,可以代理至SpringJUnit4ClassRunner。