在使用單元測試時常常會遇到某些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方法。spring
當對ServiceA的方法執行單元測試時,如ServiceA -> ServiceB,此時對ServiceB進行Mock,而後將其設置到ServiceA的屬性中;後續ServiceA調用ServiceB的方法都降獲得Mock後的結果;而對於ServiceB對象的原本的依賴本案暫且將其忽略,後續改進;數據庫
思路是在TestCase中依賴ServiceA的同時標示Mock ServiceB,待TestCase依賴注入完成後,新建ServiceB的Mock對象替換ServiceA中的ServiceB依賴;maven
@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方法中指定的結果;ide
當對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); } }
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); } }
JUnit、Mockito單元測試
AopTargetUtils工具類參考 在spring中獲取代理對象代理的目標對象工具類測試