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"));
}
}