Mock服务的使用目的在于前端测试、APP开发、前端测试人员在服务还没完备时模拟接口。
本篇里实现实时动态mock的完整代码:
https://gitee.com/475660/databand/tree/master/databand-mock-api
而不是传统使用静态mock,每次都要手动配置json,还要重新启动mock服务的方式。
如图,用户服务、其他服务没交付,账单服务交付了。那么app就通过mock模拟用户服务、其他服务接口。账单服务经mock服务中转,或者直连。
分类:
- 客户端mock:mockjs
- 服务端mock:mockserver、moco
mockjs
http://mockjs.com/
mockserver
https://github.com/mock-server/mockserver
moca
https://github.com/dreamhead/moco
模拟方式
客户端mock主要模拟:
- 1. 浏览器客户端Js-URL拦截
- 2. 模拟数据
服务端mock:
- 1. 提供真正mock-http服务
- 2. 拦截request请求,判断请求的分支(通过程序或配置文件)
- 3. 返回response,由mock-http服务提供响应数据
- 4. 或者forward,跳到其他服务url
mockserver使用
MockServer运行方式:
- . 通过java程序(我们的演示程序使用这种方式)
- . JUnit环境
- . 命令行(缺点是不能像moco那样带配置文件参数,这个下面将讲到)
- . maven plugin
- . Node.js
- . war包
配置项
Request-Matcher 匹配的参数:
- 1. method - 有GET\POST\PUT\DELETE...
- 2. path - 就是请求路径,比如\mypath\{id}
- 3. path parameters - 路径参数
- 4. query string parameters - 直接的QueryString请求参数
- 5. headers - key-values matchers
- 6. cookies - key-values matchers
- 7. body - body matchers
- 8. secure - boolean value, true for HTTPS
response返回以下任意:
- 1. status code
- 2. reason phrase
- 3. body
- 4. headers
- 5. cookies
- 6. delay
#1.简单get请求
private static void mockGet() {
mockServer.when(
request()
.withMethod("GET")
.withPath("/cart")
//设置两次后失效
//,Times.exactly(2)
//设置5秒内只能访问一次
//,Times.once(),
//TimeToLive.exactly(TimeUnit.SECONDS, 5L)
//正则,starts with "/some"
//.withPath("/some.*")//.withPath(not("/some.*"))
)
.respond(
response()
.withBody("some_response_body")
);
mockServer.when(
request()
.withMethod("GET")
.withPath("/cart1")
.withQueryStringParameters(
new Parameter("cartId", "055CA455-1DF7-45BB-8535-4F83E7266092")
)
)
.respond(
response()
.withBody("some_response_body_withQueryStringParameters")
);
}
#2.参数正则替换#
private static void mockGetPathParameter() {
mockServer.when(
request()
.withMethod("GET")
.withPath(BASEPATH_VIEW+"/cart/{cartId}")
.withPathParameters(
new Parameter("cartId", "[A-Z0-9\\-]+")
)
.withQueryStringParameters(
new Parameter("year", "2019"),
new Parameter("month", "10"),
new Parameter("userid", "[A-Z0-9\\\\-]+"))
)
.respond(
response()
.withBody("some_response_body")
);
}
#3.POST request with Body-json#
public static void mockPost() {
mockServer.when(
request()
.withMethod("POST")
.withPath(BASEPATH_VIEW)
.withBody("{username: 'user', password: 'mypassword'}")
)
.respond(
response()
.withStatusCode(200)
.withCookie(
"sessionId", "2By8LOhBmaW5nZXJwcmludCIlMDAzMW"
)
.withHeaders(
new Header("Content-Type", "application/json; charset=utf-8"),
new Header("Cache-Control", "public, max-age=86400")
)
.withBody("{ \"apply_id\": \"000001\", \"overdued\": \"Y\" }")
);
}
#4.json占位符
private static void mockBodyPlaceholder() {
mockServer.when(
request()
.withBody(
new JsonBody("{" + System.lineSeparator() +
" \"id\": 1," + System.lineSeparator() +
" \"name\": \"A_${json-unit.any-string}\"," + System.lineSeparator() +
" \"price\": \"${json-unit.any-number}\"," + System.lineSeparator() +
" \"price2\": \"${json-unit.ignore-element}\"," + System.lineSeparator() +
" \"enabled\": \"${json-unit.any-boolean}\"," + System.lineSeparator() +
" \"tags\": [\"home\", \"green\"]" + System.lineSeparator() +
"}",
MatchType.ONLY_MATCHING_FIELDS
)
)
)
.respond(
response()
.withBody("some_response_body")
);
}
#5.Responese with body-json
private static void mockResponese() {
mockServer.when(
request()
.withMethod("GET")
.withPath(BASEPATH_GET)
)
.respond(
response()
.withStatusCode(200)
//.withHeader("Content-Type", "plain/text")
.withCookie("Session", "97d43b1e-fe03-4855-926a-f448eddac32f")
.withBody(new JsonBody("{" + System.lineSeparator() +
" \"id\": 1," + System.lineSeparator() +
" \"name\": \"姓名\"," + System.lineSeparator() +
" \"price\": \"123\"," + System.lineSeparator() +
" \"price2\": \"121\"," + System.lineSeparator() +
" \"enabled\": \"true\"," + System.lineSeparator() +
" \"tags\": [\"home\", \"green\"]" + System.lineSeparator() +
"}"
))
);
}
#6.一个稍微灵活些的扩展思路:通过把各种配置项写进数据表,注入到mock服务,实现无代码mock
static ClientAndServer mockServer;
public static void main(String[] args) {
mockServer = startClientAndServer(1080);
List<Instance> mockInstances = getDataFromDb();
mockInstances(mockInstances);
}
private static List<Instance> getDataFromDb() {
List<Instance> mockInstances = new ArrayList<Instance>();
//数据行1
Instance inst = new Instance();
inst.setMethod("GET");
inst.setPath("/mypath/{id}/{type}");
Map<String,String> nullMap = new HashMap<String,String>();
Parameter[] paths = {new Parameter("id", "[A-Z0-9\\\\-]+"),new Parameter("type", "[A-Z0-9\\\\-]+")};
//inst.setQuery_string_parameters(new Parameter("year", "2019"));
inst.setPath_parameters(paths);
inst.setReq_cookie(nullMap);//--留空
inst.setReq_headers(nullMap);//--留空
inst.setReq_jsonbody("{}");//--空json
inst.setResp_body("{\r\n" +
" \"id\" : 1,\r\n" +
" \"name\" : \"姓名\",\r\n" +
" \"price\" : \"123\",\r\n" +
" \"price2\" : \"121\",\r\n" +
" \"enabled\" : \"true\",\r\n" +
" \"tags\" : [ \"tag1\", \"tag2数组项\" ]\r\n" +
"}");
Map<String,String> cookieMap = new HashMap<String,String>();
cookieMap.put("Session", "97d43b1e-fe03-4855-926a-f448eddac32f");
inst.setResp_cookie(cookieMap);
inst.setResp_headers(nullMap);
inst.setResp_statuscode(200);
//数据行2
Instance inst2 = new Instance();
inst2.setMethod("GET");
inst2.setPath("/mypath2");
Parameter[] queryParams = {new Parameter("month", "10"),new Parameter("userid", "[A-Z0-9\\\\-]+")};
inst2.setQuery_string_parameters(queryParams);
inst2.setReq_cookie(nullMap);//--留空
inst2.setReq_headers(nullMap);//--留空
inst2.setReq_jsonbody("{}");//--空json
inst2.setResp_body("{\r\n" +
" \"id\" : \"97d43b1e-fe03-4855-926a-f448eddac32f\",\r\n" +
" \"name\" : \"姓名\",\r\n" +
" \"year\" : \"2019\",\r\n" +
" \"month\" : \"10\",\r\n" +
" \"userid\" : \"id\",\r\n" +
" \"tags\" : [ \"tag3\", \"tag4\" ]\r\n" +
"}");
inst2.setResp_cookie(cookieMap);
inst2.setResp_headers(nullMap);
inst2.setResp_statuscode(200);
//把数据行注入实例列表,用于后续注入
mockInstances.add(inst);
mockInstances.add(inst2);
return mockInstances;
}
private static void mockInstances(List<Instance> mockInstances) {
for (Instance inst : mockInstances) {
List<Cookie> cookies = new ArrayList<Cookie>();
List<Header> headers = new ArrayList<Header>();
List<Cookie> resp_cookies = new ArrayList<Cookie>();
List<Header> resp_headers = new ArrayList<Header>();
//注入MOCK
injectionMock(inst, cookies, headers, resp_cookies, resp_headers);
}
}
private static void injectionMock(Instance inst, List<Cookie> cookies, List<Header> headers,
List<Cookie> resp_cookies, List<Header> resp_headers) {
for (Entry<String, String> entry : inst.getReq_cookie().entrySet()) {
cookies.add(new Cookie(entry.getKey(),entry.getValue()));
}
for (Entry<String, String> entry : inst.getReq_headers().entrySet()) {
headers.add(new Header(entry.getKey(),entry.getValue()));
}
for (Entry<String, String> entry : inst.getResp_cookie().entrySet()) {
resp_cookies.add(new Cookie(entry.getKey(),entry.getValue()));
}
for (Entry<String, String> entry : inst.getResp_headers().entrySet()) {
resp_headers.add(new Header(entry.getKey(),entry.getValue()));
}
if (inst.getPath_parameters()==null) {
mockQueryParamsNoBody(inst,cookies, headers, resp_cookies, resp_headers);
}
else {
mockPathParamsNoBody(inst,cookies, headers, resp_cookies, resp_headers);
}
}
private static void mockPathParamsNoBody(Instance inst, List<Cookie> cookies, List<Header> headers,
List<Cookie> resp_cookies, List<Header> resp_headers) {
mockServer.when(
request()
.withMethod(inst.getMethod())
.withPath(inst.getPath())
.withPathParameters(inst.getPath_parameters())
//.withQueryStringParameters(inst.getPath_parameters())
.withCookies(cookies)
.withHeaders(headers)
.withBody(new JsonBody(inst.getReq_jsonbody()))
)
.respond(
response()
.withStatusCode(inst.getResp_statuscode())
.withHeaders(resp_headers)
.withCookies(resp_cookies)
.withBody(new JsonBody(
inst.getResp_body()
))
);
}
private static void mockQueryParamsNoBody(Instance inst, List<Cookie> cookies, List<Header> headers,
List<Cookie> resp_cookies, List<Header> resp_headers) {
mockServer.when(
request()
.withMethod(inst.getMethod())
.withPath(inst.getPath())
//.withPathParameters(inst.getPath_parameters())
.withQueryStringParameters(inst.getQuery_string_parameters())
.withCookies(cookies)
.withHeaders(headers)
.withBody(new JsonBody(inst.getReq_jsonbody()))
)
.respond(
response()
.withStatusCode(inst.getResp_statuscode())
.withHeaders(resp_headers)
.withCookies(resp_cookies)
.withBody(new JsonBody(
inst.getResp_body()
))
);
}
结果:
调用:http://localhost:1080/mypath/22/2234
{
"id" : 1,
"name" : "姓名",
"price" : "123",
"price2" : "121",
"enabled" : "true",
"tags" : [ "tag1", "tag2数组项" ]
}
调用:http://localhost:1080/mypath2?month=10&userid=11
{
"id" : "97d43b1e-fe03-4855-926a-f448eddac32f",
"name" : "姓名",
"year" : "2019",
"month" : "10",
"userid" : "id",
"tags" : [ "tag3", "tag4" ]
}
moco使用
开发者郑晔是ThoughtWorks首席技术专家。moco也是根据一些配置,启动一个HTTP服务。当发起请求满足配置中的一个条件时,它就给回复一个应答。Moco的底层没有依赖于像Servlet这样的重型框架,是基于Netty框架直接编写的HTTP服务,这样一来,绕过了复杂的应用服务器,速度极快。
Moco运行方式:
https://github.com/dreamhead/moco/blob/master/moco-doc/usage.md
- 1. API通过java程序
- 2. JUnit环境
- 3. 命令行(带配置文件参数,这个好用)
- 4. maven plugin
- 5. Socket
- 6. https
最简单用法
conf.json
conf.json
[
{
"response" :
{
"text" : "Hello, Moco"
}
}
]
run:
java -jar moco-runner-1.1.0-standalone.jar http -p 12306 -c conf.json
例子
[
{
"request" :
{
"uri":"/getUser",
"method":"get",
"queries":{
"type":"pp",
"age":"3"
}
},
"response" :
{
"text" : "Hello, Moco"
}
},
{
"description":"带参数的post请求",
"request":{
"uri":"/postDemoWithParam",
"method":"post",
"forms":{
"param1":"one",
"param2":"two"
}
},
"response":{
"text":"this is post request with param 并且是form格式的"
}
},
{
"description":"带header请求",
"request": {
"uri": "/withHeader",
"method": "post",
"headers": {
"content-type": "application/json"
},
"json": {
"name": "xiaoming",
"age": "18"
}
},
"response": {
"json": {
"code": "C0",
"msg": "查询成功",
"data": {
"name": "周**",
"cid": "22************011",
"respCode": "0",
"respDesc": "一致,成功",
"detail": {
"name": "周**",
"cid": "22************011",
"driveIssueDate": "A",
"driveValidStartDate": "A",
"firstIssueDate": "A",
"validDate": "-B",
"driveCardStatus": "A",
"allowDriveCar ": "C小型汽车 ",
"driveLicenseType ": "A ",
"gender ": "1 / 男性 "
}
}
}
}
},
]
如果response响应json非常长,可以写到文件里
{
"description":"查全部",
"request":{
"uri":"/findAll",
"forms":{
"gender": "1"
}
},
"response":{
"file":"result_file/findAll.json"
}
}
分模块
当配置的路径多了,容易乱,可以分模块,比如首页模块、登陆模块
首页模块:index.json:
[
{
"description": "首页",
"request": {
"uri": "/index"
},
"response": {
"text": "hello world"
}
}
]
登录模块:login.json
[
{
"description": "登录",
"request": {
"uri": "/login"
},
"response": {
"text": "success"
}
}
]
合并:
[
{"include": "index.json"},
{"include": "login.json"}
]
MockJS客户端mock就不详细介绍了,非常简单,可以看官网说明。
实现实时动态mock的完整代码:
https://gitee.com/475660/databand/tree/master/databand-mock-api
关于作者: 王昕 在广州工作生活30余年。十多年开发经验,在Java、即时通讯、NoSQL、BPM、大数据等领域较有经验。