使用Mockito修改Bean的依賴

Honwhy發表於2019-01-19

概述

在使用單元測試時經常會遇到某些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中獲取代理物件代理的目標物件工具類

相關文章