微服务接口单测依赖问题一次性搞定

在微服务架构中,服务之间的依赖是很常见的事情。在开发过程中都是并行开发的,前端会依赖后端的接口,后端也有可能会依赖其他后端服务的接口。

项目整体提测后是没有问题的,因为大家都开发完了,也会同时部署到测试环境中。但是在开发过程中需要进行单测,单测的时候会依赖其他的服务,这个时候就需要解决这个依赖问题。

前端依赖后端接口

前端依赖后端接口,一般会提前将接口定义好,然后拉上前端同学一起评审。如果没有问题就各自去开发,那么前端同学在自测的时候是需要数据的,这个时候可以采用 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微服务 入门 实战与进阶》作者, 公众号猿天地发起人。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容