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方法
}
}
使用
- 需使用MOCK的环境,相应配置文件的remote.mock.enabled=true
- 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);
}
}
- 业务逻辑改回调用真实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()));
}
}
- 测试,开启MOCK
日志打印符合预期,返回name from mock,符合预期。 - 测试,关闭MOCK
日志打印符合预期,返回name from db,符合预期。
小结
该注解+切面的MOCK实现方法,对代码侵入性很小,仅需在需要MOCK处加一行注解,且MOCK代码可大胆放心提交,非常滴银杏。
作者:曼特宁
版权声明:本文为vipshop_fin_dev原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。