微服務介面單測依賴問題一次性搞定

猿天地發表於2021-04-18

在微服務架構中,服務之間的依賴是很常見的事情。在開發過程中都是並行開發的,前端會依賴後端的介面,後端也有可能會依賴其他後端服務的介面。

專案整體提測後是沒有問題的,因為大家都開發完了,也會同時部署到測試環境中。但是在開發過程中需要進行單測,單測的時候會依賴其他的服務,這個時候就需要解決這個依賴問題。

前端依賴後端介面

前端依賴後端介面,一般會提前將介面定義好,然後拉上前端同學一起評審。如果沒有問題就各自去開發,那麼前端同學在自測的時候是需要資料的,這個時候可以採用 Mock 的方式提供資料。

關於 Mock 的工具有很多,我們用的 YAPI,既可以管理介面資訊,又可以提供 Mock 功能。

YAPI:https://github.com/ymfe/yapi

後端依賴其他服務介面(Dubbo)

基於預定

如果不想自己 Mock 資料,可以在一開始的時候,跟需要對接的同學約定好。介面定好後先將介面寫好,返回固定的資料,釋出一下。後面在慢慢開發,這樣也能直接呼叫,並且有假資料返回。

自帶 Mock 功能

Dubbo 自帶了 Mock 功能,自定義一個 Mock 類,然後在使用的時候指定即可。

介面定義:

public interface UserRemoteService {
    ResponseData<UserResponse> getUser(Long userId);
}

Mock 類:

public class CustomUserRemoteServiceMock implements UserRemoteService {
    @Override
    public ResponseData<UserResponse> getUser(Long userId) {
        UserResponse userResponse = new UserResponse();
        userResponse.setNickname("尹吉歡");
        return Response.ok(userResponse);
    }
}

使用:

@Reference(version = DubboConstant.VERSION_V100,mock = "com.cxytiandi.CustomUserRemoteServiceMock")
private UserRemoteService userRemoteService;

自帶的 Mock 在單測的時候其實不是很方便,因為我們呼叫遠端服務的程式碼是在業務程式碼中,單測都是單獨的程式碼,如果想用 Mock 還得去改動業務程式碼,加上 mock 的資訊。很容易和本地修改的程式碼一起提交造成問題。

用在服務異常回退的場景還是比較適合的,返回靜態資料或者快取資料等。

包裝一個類實現 Mock 功能

定義一個獲取遠端物件的工廠類,負責獲取 Bean 的邏輯,使用者不需要關心內部邏輯。如果@Reference 注入了就返回 Dubbo 代理的 UserRemoteService。如果本地 Spring 中有對應的實現就返回本地的 UserRemoteService。

這樣在單測的時候,如果對方還沒有提供新服務,就可以用自己在本地建的 Mock 類實現。並且這個 Mock 類可以寫在 test 包下。

@Component
public class UserRemoteServiceBeanFactory {
    @Reference(version = DubboConstant.VERSION_V100, check=false)
    private UserRemoteService userRemoteService;

    @Autowired(required = false)
    private UserRemoteService beanUserRemoteService;
    public UserRemoteService getUserRemoteService() {
        if(Objects.nonNull(beanUserRemoteService)){
            return beanUserRemoteService;
        }
        if(Objects.nonNull(userRemoteService)){
            return userRemoteService;
        }
        throw new ApplicationException("can't find UserRemoteService");
    }
}

Mocktio Mock

Mockito 就是一個優秀的用於單元測試的 Mock 框架, 地址:https://github.com/mockito/mockito

在單測的時候,可以用 Mockito Mock 出一個遠端介面的實現,以及要返回的資料。

@MockBean
private UserRemoteService userRemoteService;
@Before
public void before() {
    Mockito.when(userRemoteService.getUser(1L))
            .thenAnswer(t -> {
                UserResponse userResponse = new UserResponse();
                userResponse.setNickname("mock name");
                return Response.ok(userResponse);
            });
}

上面的方式在單測的時候存在一個問題,雖然 Mock 了一個 Bean,但是業務類中還是用的 Dubbo 的代理類,所以得做一些特殊處理。

比如我們在 UserManagerImpl 中用了 UserRemoteService,那麼就可以先獲取 UserManagerImpl 的物件,然後在將 Mock 的 Bean set 進去,前提是得加好 setUserRemoteService 的方法。

很多時候我們在注入的時候都不會手動寫 set 方法,那你也可以用反射去 set。

 UserManagerImpl userManager = applicationContext.getBean(UserManagerImpl.class);
Field field = userManager.getClass().getDeclaredField("userRemoteService");
field.setAccessible(true);
field.set(userManager, userRemoteService);

上面雖然解決了 Mock 的 Bean 可以替換的問題,但是在每個單測中都得手動去替換,這就有點受不了啊。

所以最好是單獨封裝一個替換的類,讓使用者無感知。

@Component
public class MockitoMockDubboBeanProcessor implements BeanPostProcessor {
    @Autowired
    private ApplicationContext applicationContext;
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (StringUtils.isNotBlank(beanName) && !beanName.endsWith("ManagerImpl")) {
            return bean;
        }
        List<Field> fields = FieldUtils.getAllFieldsList(bean.getClass());
        for (Field field : fields) {
            processField(bean, field);
        }
        return bean;
    }
    private void processField(Object bean, Field field) {
        if (field.isAnnotationPresent(Reference.class)) {
            Map<String, ?> beans = applicationContext.getBeansOfType(field.getType());
            Optional<? extends Map.Entry<String, ?>> mockitoMock = beans.entrySet().stream()
                    .filter(b -> b.getValue().getClass().getName().contains("$MockitoMock$")).findFirst();
            if (mockitoMock.isPresent()) {
                field.setAccessible(true);
                try {
                    field.set(bean, mockitoMock.get().getValue());
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

主要還是對所有的 Bean 進行處理,然後根據你們的規範去做一些過濾。比如我這邊只會在 ManagerImpl 結尾的類中去呼叫遠端介面,那麼就可以直接根據這個去過濾出來我要處理的類。

然後判斷類中的屬性是不是加了 Reference 注入,然後替換 Bean 為 MockitoMock 的 Bean。

後端依賴其他服務介面(Feign)

fallback

Feign 整合 Hystrix 可實現 fallback 功能,利用這個也可以實現對方服務沒開發好,返回預設資料的功能。跟 Dubbo 的 Mock 類似。

Mocktio Mock

Mocktio 的方式跟上面一致,如果是 Feign 的話會更簡單,因為不需要單獨對類中的例項進行替換。Feign 的呼叫物件本來就在 Spring 中管理,Mocktio 直接就可以替換掉。

整合 YAPI

先說下想法吧,實現的話需要二次開發。比如前後端是通過 YAPI 來約定介面,前端在自測的時候也是通過 YAPI 的 Mock 功能獲取 Mock 的資料。

如果用 Feign 進行遠端呼叫,說明你們的服務內部通訊就是基於 Http 方式。那麼是否可以和前端一樣,正常的時候走服務呼叫,單測的時候可以 YAPI 的 Mock 介面呢?這樣也就不用自己在單測中去 Mock 資料了。

要做的話基於 Feign 底層擴充套件下,通過配置來控制,我這裡只是給大家提供個思路,感興趣的可以動手試試,然後投稿下,哈哈。

關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud微服務-全棧技術與案例解析》, 《Spring Cloud微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。

相關文章