某个服务或前端依赖一个服务接口,该接口可能依赖多个低层服务或模块,或第三方接口,这种情况下需要搭建多个底层模块多套测试环境,比较痛苦,如果mock掉第一级的服务接口,可以节约不少人力,同时规避了可能由第三方服务导致的问题。
目前常见服务或接口协议主要两种,一种是RPC,另一种是HTTP/HTTPS,mock原理都类似,要么是修改原服务地址为Mock服务地址,要么是拦截原服务的请求Mock返回值,总之就是构造一个假的服务,替代原有服务。
只介绍下HTTP/HTTPS协议的mock实现,RPC不做深究,原理都类似。Mock实现常见两种方式,一种是通过代理抓取待测服务请求并控制返回值;另一种是直接将待测服务指向Mock服务地址,替代下游原始真实服务。
第一种通过替换待测服务为Mock Gateway地址抓取请求并控制返回值来实现,(或者简单点,直接用Mock Service地址来替换待测服务地址也可以,更简单)通过Gateway网关转发请求,下游再设具体mock服务,可以是一个mock服务直接返回预期的mock值,也可以是proxy服务继续代理请求到其他地址,或redirect服务转发到某个特殊的地址等等方式。
如果自己搭建,建议使用java技术栈,Mock Gateway和Mock Service可以使用springcloud或springboot来实现,比较简单,mock策略和数据可以使用mysql或redis或es来存储,或者放到内存中也是可以的。
第二种直接使用正向代理代理请求,拦截请求,常用工具有charles、fiddler,mitmproxy等,工具比较成熟,直接使用即可,不做深入讨论,如果持久化Mock数据,建议使用mitmproxy,技术栈是python,可自行研究
介绍下第一种mock方案实践方案,技术栈选用springcloud+zuul+mybatis+mysql+keytools
#存储mock规则,包含所有mock、proxy、redirect等规则数据
-- auto-generated definition
create table mock_app
(
id int auto_increment,
name varchar(200) null,
description varchar(200) null,
request_type varchar(20) null,
request_uri varchar(200) null,
request_query longtext null,
request_body longtext null,
request_header longtext null,
mock_data longtext null,
redirect_type varchar(200) null,
redirect_url varchar(200) null,
redirect_query longtext null,
redirect_body longtext null,
redirect_header longtext null,
proxy_url_perfix varchar(200) null,
is_active tinyint(1) null,
create_time timestamp not null,
update_time timestamp not null,
approver varchar(200) not null,
constraint mock_app_id_uindex
unique (id)
);
alter table mock_app
add primary key (id);
#存储mock策略,mock_app表中同一条mock数据根据mock策略返回不同的值
-- auto-generated definition
create table mock_strategy
(
id int auto_increment,
name varchar(200) not null,
description varchar(200) not null,
mock_response_strategy int not null,
create_time timestamp not null,
update_time timestamp not null,
approver varchar(200) not null,
constraint mock_strategy_id_uindex
unique (id)
);
alter table mock_strategy
add primary key (id);
package com.personal.mock;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@MapperScan(basePackages = "com.personal.mock.dao")
@EnableZuulProxy
public class MockApplication {
public static void main(String[] args) {
SpringApplication.run(MockApplication.class, args);
}
}
#properties配置文件
#gateway服务端口
server.port=10086
#添加zuul拦截条件
zuul.routes.root.path=/*
zuul.routes.root.url=http://127.0.0.1:10086/
#数据库相关配置
spring.datasource.url = jdbc:mysql://localhost:3306/mock?characterSet=utf8mb4&useSSL=false
spring.datasource.username = root
spring.datasource.password = QWER1234qwer
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.filters=stat
spring.datasource.maxActive=20
spring.datasource.initialSize=1
spring.datasource.maxWait=60000
spring.datasource.minIdle=1
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=true
spring.datasource.testOnReturn=true
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20
#下游服务配置
mock.request.address=http://127.0.0.1:10010/mock/request
mock.response.address=http://127.0.0.1:10010/mock/response
proxy.address=http://127.0.0.1:10011/proxy
redirect.address=http://127.0.0.1:10012/redirect
package com.personal.mock.filter;
import com.alibaba.fastjson.JSONObject;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.personal.mock.common.MockStrategyEnum;
import com.personal.mock.po.MockApp;
import com.personal.mock.route.MockZuulRoute;
import com.personal.mock.route.MockZuulRouteLocator;
import com.personal.mock.service.FilterService;
import com.personal.mock.service.RequestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.Map;
import static com.personal.mock.common.Constant.*;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* author: zhaoxu
* date: 2019/4/30 上午10:07
*/
@Component
public class MockPreFilter extends ZuulFilter {
private static Logger logger = LoggerFactory.getLogger(MockPreFilter.class);
@Value(value = "${mock.request.address}")
private String mockRequestAddress;
@Value(value = "${mock.response.address}")
private String mockResponseAddress;
@Value(value = "${proxy.address}")
private String proxyAddress;
@Value(value = "${redirect.address}")
private String redirectAddress;
@Autowired
FilterService filterService;
@Autowired
RequestService requestService;
@Autowired
MockZuulRouteLocator mockZuulRouteLocator;
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return 4;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest httpServletRequest = ctx.getRequest();
logger.info(String.valueOf(requestService.getRequestHeader()));
logger.info(requestService.getMethod());
logger.info(requestService.getQueryString());
logger.info(requestService.getRequestURI());
logger.info(String.valueOf(requestService.getRequestBody()));
logger.info("request uri: "+httpServletRequest.getRequestURI());
Map map = filterService.getFilterInfo(httpServletRequest);
if (null != map){
Iterator iterator = map.keySet().iterator();
MockApp mockApp = iterator.next();
Integer mockStrategyId = map.get(mockApp);
String path = mockApp.getRequestUri();
Integer mockAppId = mockApp.getId();
String url = null;
try{
MockStrategyEnum mockStrategyEnum = MockStrategyEnum.getMockStrategyByStrategyId(mockStrategyId);
switch (mockStrategyEnum) {
case MOCK_RESPONSE_DIRECT:
url = mockResponseAddress;
logger.info("【正常,gateway转发给下级服务处理】" +url);
break;
case MOCK_REQUEST_RETURN:
url = mockRequestAddress;
logger.info("【正常,gateway转发给下级服务处理】" +url);
break;
case REQUEST_REDIRECT:
url = redirectAddress;
logger.info("【正常,gateway转发给下级服务处理】" +url);
break;
case PROXY:
url = proxyAddress;
logger.info("【正常,gateway转发给下级服务处理】" +url);
break;
}
} catch(NullPointerException e){
logger.error("【异常,没有匹配到策略,详情如下:】");
logger.error("【mock_app.id=%d 对应的mock_strategy.mock_response_strategy配置异常,请检查】");
}
MockZuulRoute mockZuulRoute = new MockZuulRoute(mockAppId.toString(),path,url);
mockZuulRouteLocator.updateRoutes(mockZuulRoute);
ctx.addZuulRequestHeader(MOCK_STRATEGY_ID,mockStrategyId.toString());
ctx.addZuulRequestHeader(MOCK_APP_ID,mockAppId.toString());
ctx.addZuulRequestHeader(REQUEST_URI,httpServletRequest.getRequestURI());
} else {
logger.error("【异常,没有匹配到mock数据,请检查】");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
JSONObject jsonObject = new JSONObject();
jsonObject.put("errorMessage","【呃呃呃, 你的请求没有匹配到数据库的mock数据,请检查】");
ctx.setResponseBody(jsonObject.toJSONString());
ctx.getResponse().setContentType("application/json;charset=UTF-8");
}
return null;
}
}
mock-service、mock-proxy服务与mock-gateway代码结构类似,去掉请求过滤器即可
po层和dao层的代码就不粘贴了,类似与mybatis,后续我会把代码放到github上