• MOCK远程API调用的简单实现


    背景

    我们在平时的日常开发工作中,经常会需要调用其他服务的API。当我们把代码敲完后,需要验证一下,诸如入参传得对不对、出参是否符合我的预期等,这就需要与对方开发同学进行开发联调。但是,对方服务不一定具备开发联调的条件,此时,就需要我们对API的出参进行MOCK。

    MOCK有多种方法实现,最简单的就是再copy一个方法,写死出参,替换掉真实的调用API的逻辑。此方案有个缺点,需要删除原调用逻辑,如果不小心将MOCK代码提交,会对测试环境乃至生产环境造成影响。

    本篇将介绍一种可大胆放心提交代码的MOCK方案,实现原理是注解+切面。

    查询用户信息API

    模拟一个RPC服务端

    /**
     * 模拟一个RPC服务端
     */
    @Component
    @Slf4j
    public class UserServer {
        /**
         * 模拟查询用户信息
         */
        public UserInfoResp getUserInfo(int userId) {
            log.info("----------调用真实API,RPC服务端");
            if (userId < 1) {
                return UserInfoResp.builder()
                        .code(500).msg("userId < 1")
                        .build();
            }
            UserInfoResp.UserInfo userInfo = selectUserInfoFromDb(userId);
            if (userInfo == null) {
                return UserInfoResp.builder()
                        .code(500).msg("user not exist")
                        .build();
            }
            return UserInfoResp.builder()
                    .code(200).msg("success").userInfo(userInfo)
                    .build();
        }
        /**
         * 模拟从DB查询用户信息
         * select * from user_info where user_id = ?
         */
        private static UserInfoResp.UserInfo selectUserInfoFromDb(int userId) {
            if (userId < 10) { // 模拟userId < 10的用户不存在
                return null;
            }
            return UserInfoResp.UserInfo.builder()
                    .userId(userId).userName("name from db").age(30).sex("男")
                    .build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    /**
     * 模拟查询用户信息的出参
     */
    @Builder
    @Data
    public class UserInfoResp {
        private int code;
        private String msg;
        private UserInfo userInfo;
        @Builder
        @Data
        public static class UserInfo {
            private int userId;
            private String userName;
            private int age;
            private String sex;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    RPC客户端

    /**
     * RPC客户端
     */
    @Component
    @Slf4j
    public class UserClient {
        @Autowired
        private UserServer userServer; // RPC服务端
        public UserInfoResp getUserInfo(int userId) {
            log.info("----------调用真实API,RPC客户端");
            return userServer.getUserInfo(userId);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    业务逻辑

    @Service
    public class MyUserServiceImpl implements MyUserService {
        @Autowired
        private UserClient userClient; // RPC客户端
        /**
         * 业务逻辑,根据用户ID获取用户名
         */
        @Override
        public String getUserName(int userId) throws OspException {
            UserInfoResp resp = userClient.getUserInfo(userId); // 调用真实API
            return Optional.of(resp)
                    .filter(u -> resp.getCode() == 200)
                    .map(UserInfoResp::getUserInfo)
                    .map(UserInfoResp.UserInfo::getUserName)
                    .orElseThrow(() -> new OspException(resp.getMsg()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    此时,一个简单的获取用户名的服务就实现好了,简单测试一下,返回name from db,符合预期。
    在这里插入图片描述

    简单粗暴的MOCK

    copy一个方法,写死出参,替换掉真实的调用API的逻辑。

    MOCK RPC客户端

    /**
     * MOCK RPC客户端
     */
    @Slf4j
    public class MockUserClient {
        public static UserInfoResp getUserInfo(int userId) {
            log.info("----------调用MOCK API");
            UserInfoResp.UserInfo userInfo = UserInfoResp.UserInfo.builder()
                    .userId(userId).userName("name from mock").age(30).sex("男")
                    .build();
            return UserInfoResp.builder()
                    .code(200).msg("success").userInfo(userInfo)
                    .build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    业务逻辑改调用MOCK API

    @Service
    public class MyUserServiceImpl implements MyUserService {
        /**
         * 业务逻辑,根据用户ID获取用户名
         */
        @Override
        public String getUserName(int userId) throws OspException {
    //        UserInfoResp resp = userClient.getUserInfo(userId); // 调用真实API
            UserInfoResp resp = MockUserClient.getUserInfo(userId); // 调用MOCK API
            return Optional.of(resp)
                    .filter(u -> resp.getCode() == 200)
                    .map(UserInfoResp::getUserInfo)
                    .map(UserInfoResp.UserInfo::getUserName)
                    .orElseThrow(() -> new OspException(resp.getMsg()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    简单测试一下,返回name from mock,符合预期。
    在这里插入图片描述

    相对优雅的MOCK

    采用注解+切面。

    定义注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RuntimeMock {
        /**
         * (Required) 对该接口进行运行时 mock 的目标类
         */
        Class<?> mockClass();
        /**
         * (Required) 对该接口进行运行时 mock 的目标类接口名
         */
        String mockMethod();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    定义切面

    @Aspect
    @Component
    @Profile({"development", "integratetest"}) // development开发环境,integratetest测试环境,生产环境该切面不生效
    @Slf4j
    public class RemoteMockAspect {
        @Value("#{new Boolean('${remote.mock.enabled}')}") // 是否使用MOCK,读取properties文件的值
        private boolean remoteMockEnabled = false;
        @Around("@annotation(com.mock.RuntimeMock)") // 对RuntimeMock注解环绕增强
        public Object mock(ProceedingJoinPoint point) throws Throwable {
            if (!remoteMockEnabled) { // 不使用MOCK
                log.info("----------不使用MOCK");
                return point.proceed();
            }
            try {
                log.info("----------使用MOCK");
                return doMock(point); // 使用MOCK
            } catch (Throwable e) {
                return point.proceed();
            }
        }
        private Object doMock(ProceedingJoinPoint point) throws Throwable {
            Signature signature = point.getSignature();
            Class<?> targetClass = point.getTarget().getClass();
            Class<?>[] paramTypes = ((MethodSignature) signature).getParameterTypes();
            Method targetMethod = targetClass.getMethod(signature.getName(), paramTypes);
    
            RuntimeMock runtimeMock = targetMethod.getAnnotation(RuntimeMock.class);
            Class<?> mockClass = runtimeMock.mockClass(); // mock 的目标类
            String mockMethod = runtimeMock.mockMethod(); // mock 的目标类接口名
    
            Method method = mockClass.getMethod(mockMethod, paramTypes);
            Object instance = Modifier.isStatic(method.getModifiers()) ? null : mockClass.newInstance();
            return method.invoke(instance, point.getArgs()); // invoke进MOCK方法
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    使用

    1. 需使用MOCK的环境,相应配置文件的remote.mock.enabled=true
      在这里插入图片描述
    2. RPC客户端加注解
    /**
     * RPC客户端
     */
    @Component
    @Slf4j
    public class UserClient {
        @Autowired
        private UserServer userServer; // RPC服务端
        @RuntimeMock(mockClass = MockUserClient.class, mockMethod = "getUserInfo") // 加注解
        public UserInfoResp getUserInfo(int userId) {
            log.info("----------调用真实API,RPC客户端");
            return userServer.getUserInfo(userId);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 业务逻辑改回调用真实API
    @Service
    public class MyUserServiceImpl implements MyUserService {
        @Autowired
        private UserClient userClient; // RPC客户端
        /**
         * 业务逻辑,根据用户ID获取用户名
         */
        @Override
        public String getUserName(int userId) throws OspException {
            UserInfoResp resp = userClient.getUserInfo(userId); // 调用真实API
            return Optional.of(resp)
                    .filter(u -> resp.getCode() == 200)
                    .map(UserInfoResp::getUserInfo)
                    .map(UserInfoResp.UserInfo::getUserName)
                    .orElseThrow(() -> new OspException(resp.getMsg()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 测试,开启MOCK
      日志打印符合预期,返回name from mock,符合预期。
      在这里插入图片描述
      在这里插入图片描述
    2. 测试,关闭MOCK
      日志打印符合预期,返回name from db,符合预期。
      在这里插入图片描述
      在这里插入图片描述

    小结

    该注解+切面的MOCK实现方法,对代码侵入性很小,仅需在需要MOCK处加一行注解,且MOCK代码可大胆放心提交,非常滴银杏。

    作者:曼特宁

  • 相关阅读:
    计算机网络-计算机网络体系结构-应用层
    跳闸、合闸位置监视继电器HJTHW-E002J/DC220V
    MySQL的游标遍历
    YOLOV8的tensorrt部署详解(目标检测模型-CUDA)
    移动Web第三天 1 移动端特点 && 2 百分比布局 && 3 Flex布局
    【Python】语言学习
    【PyTorch深度学习项目实战100例】—— 基于ResNet50实现多目标美味蛋糕图像分类 | 第51例
    雷神轮胎携手JBL 演绎科技降噪、感受非凡音悦
    ansible部署MySQL主从
    为什么心脏长在左边?
  • 原文地址:https://blog.csdn.net/vipshop_fin_dev/article/details/127830411