一、背景
某个服务或前端依赖一个服务接口,该接口可能依赖多个低层服务或模块,或第三方接口,这种情况下需要搭建多个底层模块多套测试环境,比较痛苦,如果mock掉第一级的服务接口,可以节约不少人力,同时规避了可能由第三方服务导致的问题。
目前常见服务或接口协议主要两种,一种是RPC,另一种是HTTP/HTTPS,mock原理都类似,要么是修改原服务地址为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实践实例
介绍下第一种mock方案实践方案,技术栈选用springcloud+zuul+mybatis+mysql+keytools
- 工程1:mock-gateway用于实现拦截请求,接受到请求后,充当路由功能
- 工程2:mock-service用于接受mock-gateway转发来的请求,指定mock规则和mock数据返回
- 工程3:proxy-service用于接受mock-gateway转发来的请求,继续透传请求或其他mock规则改变请求
- 创建数据库和表
-
#存储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);
-
- mock-gateway
- 直接创建一个springcloud工程,不会的自己搜索下,很简单,官网直接可以生产工程目录
- 添加springcloud主类
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<MockApp, Integer> map = filterService.getFilterInfo(httpServletRequest); if (null != map){ Iterator <MockApp>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上