概述
在使用單元測試時經常會遇到某些dependency依賴了外部資源,或者想主動繞過真正的方法執行mock返回結果而快速得到單元測試最終的期望結果,可能有以下兩種場景,
對於TestCase A,設單元測試的方法是Service A的execute1方法和execute2方法,在執行execute1和execute2方法時都會呼叫ServiceB的不同方法,即ServiceA依賴了ServiceB;一個場景是完全對ServiceB進行Mock,如單元測試ServiceA#execute1方法時都通過Mock返回結果;一個場景是部分ServiceB的方法執行真實的業務邏輯(如查詢資料庫),一部分方法執行Mock返回結果,或Spy,如如單元測試ServiceA#execute2方法時,只mock ServiceB#b2結果,真正執行ServiceB#b1方法。
對TestCase的Service的依賴Bean的完全Mock
當對ServiceA的方法執行單元測試時,如ServiceA -> ServiceB,此時對ServiceB進行Mock,然後將其設定到ServiceA的屬性中;後續ServiceA呼叫ServiceB的方法都降得到Mock後的結果;而對於ServiceB物件的本來的依賴本案暫且將其忽略,後續改進;
思路是在TestCase中依賴ServiceA的同時標示Mock ServiceB,待TestCase依賴注入完成後,新建ServiceB的Mock物件替換ServiceA中的ServiceB依賴;
@TestExecutionListeners({MockitoDependencyInjectionTestExecutionListener.class})
public class AServiceMockTest extends BaseTest {
@Mock
private BService bservice;
@Autowired
private AService aservice;
@Before
public void setup(){
doReturn("mock").when(bservice).b1();
}
@Test
public void test() {
a.execute1();
}
}
@Service
public class AServiceImpl implements AService {
@Autowired
private BService bservice;
@Override
public String execute1() {
return bservice.b1(); //will return mock after Mock
}
}
當a.execute()執行時將呼叫aservice的屬性bservice的b1方法,返回結果就是在setup方法中指定的結果;
監聽TestCase的Service的依賴Bean
當對ServiceA進行單元測試時,依賴了ServiceB,需要獲取ServiceB的b1方法的真正執行結果,Mock b2方法的結果,此時可以採用Spy方式;由於ServiceA依賴了ServiceB,而這個屬性可能是個AopProxy物件,並不能直接使用Mockito.mock(bservice)或者Mockito.spy(bservice),所以這裡@Spy註解指定的是實現類,通過MockitoDependencyInjectionTestExecutionListener處理後,獲得一個Spy物件,同時這個Spy物件設定到bservice(AopProxy物件)中去;
@TestExecutionListeners({MockitoDependencyInjectionTestExecutionListener.class})
public class AServiceMockTest extends BaseTest {
@Spy
private BServiceImpl bserviceImpl;
@Autowired
private AService aservice;
@Before
public void setup(){
doReturn(true).when(bserviceImpl).b2(any(String.class));
}
@Test
public void test() {
a.execute();
}
}
@Service
public class AServiceImpl implements AService {
@Autowired
private BService bservice;
@Override
public boolean execute2() {
String str = bservice.b1();
return bservice.b2(str);
}
}
MockitoDependencyInjectionTestExecutionListener的實現
public class MockitoDependencyInjectionTestExecutionListener extends DependencyInjectionTestExecutionListener {
private Set<Field> injectFields = new HashSet<>();
private Map<String,Object> mockObjectMap = new HashMap<>();
@Override
protected void injectDependencies(TestContext testContext) throws Exception {
super.injectDependencies(testContext);
init(testContext);
}
/**
* when A dependences on B
* mock B or Spy on targetObject of bean get from Spring IoC Container whose type is B.class or beanName is BImpl
* @param testContext
*/
private void init(TestContext testContext) throws Exception {
AutowireCapableBeanFactory factory =testContext.getApplicationContext().getAutowireCapableBeanFactory();
Object bean = testContext.getTestInstance();
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if(annotation instanceof Mock){
Class<?> clazz = field.getType();
Object object = Mockito.mock(clazz);
field.setAccessible(true);
field.set(bean, object);
mockObjectMap.put(field.getName(), object);
} else if(annotation instanceof Spy) {
Object fb = factory.getBean(field.getName()); //may be a proxy that can not be spy because $Proxy is final
Object targetSource = AopTargetUtils.getTarget(fb);
Object spyObject = Mockito.spy(targetSource);
if (!fb.equals(targetSource)) { //proxy
if (AopUtils.isJdkDynamicProxy(fb)) {
setJdkDynamicProxyTargetObject(fb, spyObject);
} else { //cglib
setCglibProxyTargetObject(fb, spyObject);
}
} else {
mockObjectMap.put(field.getName(), spyObject);
}
field.setAccessible(true);
field.set(bean, spyObject);
}else if (annotation instanceof Autowired){
injectFields.add(field);
}
}
}
for(Field field: injectFields) {
field.setAccessible(true);
Object fo = field.get(bean);
if (AopUtils.isAopProxy(fo)) {
Class targetClass = AopUtils.getTargetClass(fo);
if(targetClass ==null)
return;
Object targetSource = AopTargetUtils.getTarget(fo);
Field[] targetFields =targetClass.getDeclaredFields();
for(Field targetField : targetFields){
targetField.setAccessible(true);
if(mockObjectMap.get(targetField.getName()) ==null){
continue;
}
ReflectionTestUtils.setField(targetSource,targetField.getName(), mockObjectMap.get(targetField.getName()));
}
} else {
Object realObject = factory.getBean(field.getType());
if(null != realObject) {
Field[] targetFields = realObject.getClass().getDeclaredFields();
for(Field targetField : targetFields){
targetField.setAccessible(true);
if(mockObjectMap.get(targetField.getName()) ==null){
continue;
}
ReflectionTestUtils.setField(fo,targetField.getName(), mockObjectMap.get(targetField.getName()));
}
}
}
}
}
private void setCglibProxyTargetObject(Object proxy, Object spyObject) throws NoSuchFieldException, IllegalAccessException {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).setTarget(spyObject);
}
private void setJdkDynamicProxyTargetObject(Object proxy, Object spyObject) throws NoSuchFieldException, IllegalAccessException {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
((AdvisedSupport) advised.get(aopProxy)).setTarget(spyObject);
}
}
附
maven依賴
JUnit、Mockito
AopTargetUtils
AopTargetUtils工具類參考 在spring中獲取代理物件代理的目標物件工具類