mock使用
mock主要在单元测试的时候用来模拟外部依赖接口的返回,即method stub的作用。 一般而言,在常见的单元测试的编写中,通过mock外部依赖来使得待测试的代码能往下执行。
在单测中,莫过于以下三个步骤,
- 确定目标
- 构造条件
- 验证返回
mock场景
- mock对象
- mock方法 模拟返回或抛出异常
- mock私有属性
创建mock对象
- 被mock的目标对象
public class Target {
private String name = "default";
public String getName() {
return name;
}
public String someMethod(String arg) {
return arg + "!!!";
}
static String staticMethod() {
return "static";
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
sb.append("\"name\":\"")
.append(name).append('\"');
sb.append('}');
return sb.toString();
}
}
- 编码方式
Target target = BDDMockito.mock(Target.class);
- 注解方式
public class TestMockito {
@Mock
private Target target;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this); // 使用注解方式需要初始化或者在类上加上注解@RunWith(MockitoJUnitRunner.class)
}
@Test
public void testxxx(){
// ...
}
}
- 注入mock对象到其他对象中
@InjectMocks
private XXService xxService;
@Mock
private A a;
@Mock
private B b;
@InjectMocks将@Mock注解的对象注入到XXService对象中,注入的方式有三种:构造器注入、setter注入和field注入(优先级即为此顺序)。
Again, note that @InjectMocks will only inject mocks/spies created using the @Spy or @Mock annotation.
@InjectMocks也需和 MockitoAnnotations.initMocks或MockitoJUnitRunner一同使用。
stub方法
- stub来模拟方法返回,Mockito.when()
import static org.mockito.Mockito.*;
@Test
public void testIteratorStyleStubbing() {
// when(target.someMethod("a")).thenReturn("b");
when(target.someMethod(Mockito.anyString())).thenReturn("a").thenReturn("b");
System.out.println(target.someMethod("1")); // a
System.out.println(target.someMethod("1")); // b
System.out.println(target.someMethod("1")); // b
}
@Test
public void testIteratorStyleStubbing2() {
when(target.someMethod(anyString())).thenReturn("a", "b", "c", "d");
System.out.println(target.someMethod("1")); // a
System.out.println(target.someMethod("1")); // b
System.out.println(target.someMethod("1")); // c
System.out.println(target.someMethod("1")); // d
System.out.println(target.someMethod("1")); // d
}
@Test
public void testStubingWithCallback() {
when(target.someMethod(anyString())).thenAnswer(stubbing -> "callback with args:" + Arrays.asList(stubbing.getArguments()).toString
());
System.out.println(target.someMethod("a"));
}
@Test
public void doXX() {
doThrow(new RuntimeException()).when(target).someMethod(anyString());
try {
target.someMethod("a");
} catch (Exception e) {
Assert.assertEquals(e.getClass(), RuntimeException.class);
}
// doNothing()
// doReturn()
// doAnswer()
// doCallRealMethod()
}
- 静态方法和私有方法
因为Mockito是通过CGLIB继承目标类来达到代理的目的,所有无法重写静态方法和private方法,所以无法mock。
mock私有属性
通过反射的方式给attribute赋值,有两个工具类:
- spring的ReflectionTestUtils.setField()
- Mockito的FieldSetter
二者都是基于反射的field赋值。
@Test
public void mockPrivateAttr() {
Target injectTarget = new Target();
// ReflectionTestUtils.setField(injectTarget, "name", "mockName");
try {
FieldSetter.setField(injectTarget, Target.class.getDeclaredField("name"), "mockName");
System.out.println(injectTarget.getName());
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
原理初探
Mock是基于代理来完成模拟的,准确的说,应该是基于动态代理。
代理和动态代理
代理对象作为目标对象的影子,基于委托的机制、利用里式替换原则达到对目标对象行为的拦截。在面向抽象(接口)的编程中,可以轻松地使用代理对象来模拟真实对象的行为。代理就是将目标对象藏在代理对象的内部,由代理来决定是否要将动作施加在目标对象之上,且代理对象可以在大多数情况下作为目标对象的替身代表目标对象承担责任。
动态代理是相对静态代理而言的,静态代理就是代理对象需要在代理之前手工编写代理类,是为静态代理,动态代理的动态特性体现在运行时,也就是由代码在执行过程中动态地去创建匿名的代理类,本质上和静态代理一样,只是利用一系列动态技术将编写代理类自动化罢了。
AOP就是很典型的动态代理的运用场景,Mock也是动态代理。
生成代理对象和拦截方法调用
生成动态代理的方式,一般来说有两种,一种是JDK的Proxy动态代理的实现,另外一种是基于字节码操作的CGLIB库的使用,二者的区别是,前者基于接口实现和委托,后者则是基于继承。在接口和子类的角度,同一个接口的实现类可以相互替换,子类可以替换父类,从而使得代理类能够在面向抽象的编程中,作为目标对象的替身起作用。
拦截方法的调用则是将对于代理对象的所有方法调用,经由拦截器来处理,具体执行逻辑有拦截器的逻辑来实现。如Proxy方式的InvocationHandler,MethodInterceptor,。
- example:
// 目标类 注意必须实现接口
public class Target implements TargetInterface {
private String name;
@Override
public String doSomeThing(String s) {
return s + s;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// JDK动态代理
public class JDKProxyTest {
public static void main(String[] args) {
// JDK动态代理的方式的缺点在于 目标类必须实现了接口,否则无法进行代理(不然无法替换)
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(Main.class.getClassLoader(),
Target.class.getInterfaces(), (proxy1, method, args1) -> {
return "hello world";
});
System.out.println(proxy.doSomeThing("ss")); // hello world
}
}
- CGLIB example
public class CglibTest {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Target.class);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> "helloworld");
Target proxy = (Target) enhancer.create();
System.out.println(proxy.doSomeThing("sddd")); // helloworld
}
}
基于CGLIB模拟Mock功能
Mock
- 核心类
package com.demo.minimock;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class MiniMock {
public static <T> T mock(Class<T> type) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
// 根据入参匹配container的备选
String key = obj.getClass().getName() + "_" + method.getName();
Object value = RetValueContainer.match(key);
if (null == value) {
RetValueContainer.setMatcherKey(key);
}
return value;
});
return type.cast(enhancer.create());
}
public static <T> MiniStubbing when(T call) {
return new MiniStubbing();
}
}
- stubbing
package com.demo.minimock;
/**
* stubbing类
*/
public class MiniStubbing {
public MiniStubbing thenReturn(Object result) {
if (RetValueContainer.getKey() != null) {
RetValueContainer.add(RetValueContainer.getKey(), result);
} else {
throw new UnsupportedOperationException();
}
return this;
}
}
- 目标返回值容器
package com.demo.minimock;
import java.util.concurrent.ConcurrentHashMap;
/**
* 缓存stubbing的返回值
*/
public class RetValueContainer {
private static String matcherKey = null;
private static final ConcurrentHashMap<String, Object> candidateMap = new ConcurrentHashMap<>();
public static void setMatcherKey(String matcherKey) {
RetValueContainer.matcherKey = matcherKey;
}
public static String getKey() {
return matcherKey;
}
public static void add(String key, Object value) {
candidateMap.put(key, value);
}
public static Object match(String hashCode) {
return candidateMap.get(hashCode);
}
}
Test
- 目标类
public class Target {
private String a = "default";
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
public String getXXX(String x) {
return x + x + x;
}
}
- 测试
public class TestMiniMock {
@Test
public void testMock() {
Target target = MiniMock.mock(Target.class);
MiniMock.when(target.getA()).thenReturn("1000");
MiniMock.when(target.getXXX("")).thenReturn("xxx");
System.out.println(target.getA());
System.out.println(target.getA());
System.out.println(target.getA());
System.out.println(target.getXXX("a"));
}
}