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();
    }
}
/**
 * 模拟查询用户信息的出参
 */
@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;
    }
}

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);
    }
}

业务逻辑

@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()));
    }
}

此时,一个简单的获取用户名的服务就实现好了,简单测试一下,返回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();
    }
}

业务逻辑改调用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()));
    }
}

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

相对优雅的MOCK

采用注解+切面。

定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RuntimeMock {
    /**
     * (Required) 对该接口进行运行时 mock 的目标类
     */
    Class<?> mockClass();
    /**
     * (Required) 对该接口进行运行时 mock 的目标类接口名
     */
    String mockMethod();
}

定义切面

@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. 需使用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. 业务逻辑改回调用真实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. 测试,开启MOCK
    日志打印符合预期,返回name from mock,符合预期。
    在这里插入图片描述
    在这里插入图片描述
  2. 测试,关闭MOCK
    日志打印符合预期,返回name from db,符合预期。
    在这里插入图片描述
    在这里插入图片描述

小结

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

作者:曼特宁


版权声明:本文为vipshop_fin_dev原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。