在微服務架構中,服務之間的依賴是很常見的事情。在開發過程中都是並行開發的,前端會依賴後端的介面,後端也有可能會依賴其他後端服務的介面。
專案整體提測後是沒有問題的,因為大家都開發完了,也會同時部署到測試環境中。但是在開發過程中需要進行單測,單測的時候會依賴其他的服務,這個時候就需要解決這個依賴問題。
前端依賴後端介面
前端依賴後端介面,一般會提前將介面定義好,然後拉上前端同學一起評審。如果沒有問題就各自去開發,那麼前端同學在自測的時候是需要資料的,這個時候可以採用 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微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。