moco框架
一、Moco 简介与核心概念
Moco 是一个开源框架,其核心设计理念是 “配置即API”。它通过简单的配置(JSON 或 Java API)来定义请求和响应,支持 HTTP、HTTPS、Socket 等多种协议,可以模拟延迟、Cookie、重定向等复杂场景
Moco 的优势在于:
简单易用:无需编写大量代码,通过 JSON 文件即可配置 API
轻量独立:作为一个独立的 JAR 包运行,不依赖 Servlet 容器,启动速度快
灵活强大:支持 REST、HTTP、Socket 等协议,可模拟各种异常和复杂场景
易于集成:可以轻松集成到 JUnit、TestNG 等单元测试框架中,成为持续集成 (CI) 流程的一环
📦 二、环境准备与依赖导入
使用 Moco 1.5.0 前,你需要确保系统中已安装 Java 运行环境 (JRE 1.6+)
,推荐使用 Java 8 或更高版本以获得更好的兼容性。
- 直接使用独立 JAR 包(推荐用于快速测试)
这是最简单的方式,无需构建工具,适合独立运行 Mock 服务。
下载 Moco Runner:
从 Maven 仓库](https://github.com/dreamhead/moco)下载 moco-runner-1.5.0-standalone.jar
下载
准备配置文件:
创建一个 JSON 文件(如 moco.json)来定义你的接口规则。
启动 Moco Server:
在命令行中执行以下命令:
java -jar moco-runner-1.5.0-standalone.jar http -p 12306 -c moco.json
http: 指定协议为 HTTP。
-p 12306: 指定服务端口为 12306。
-c moco.json: 指定配置文件
-g参数指定主配置文件
- 通过构建工具引入依赖(用于集成到项目测试)
如果你希望将 Moco 集成到现有的 Java 项目(如使用 JUnit 进行单元测试),可以通过 Maven 或 Gradle 引入依赖。
Maven 依赖配置
在你的项目 pom.xml文件中添加以下依赖:
<dependency>
<groupId>com.github.dreamhead</groupId>
<artifactId>moco-core</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
这里通常使用 moco-core,scope设置为 test,表示仅在测试时使用。
Gradle 依赖配置
在你的 build.gradle文件中的 dependencies块添加:
testImplementation ‘com.github.dreamhead:moco-core:1.5.0’
3. 解决中文乱码问题
如果遇到响应中文乱码,可以在启动命令中加入 -Dfile.encoding=utf-8参数
:
java -Dfile.encoding=utf-8 -jar moco-runner-1.5.0-standalone.jar http -p 12306 -c moco.json
或者在 JSON 配置中,为响应明确指定 UTF-8 编码的 Content-Type:
{
"response": {
"headers": {
"Content-Type": "text/plain;charset=UTF-8"
},
"text": "这里是中文响应"
}
}
🧪 三、Mock 各种类型接口
Moco 的强大之处在于它能灵活模拟各种类型的接口。以下示例均以 JSON 配置方式展示,你也可以使用等效的 Java API
- 简单的文本接口
这是一个最基本的 GET 请求示例,返回纯文本。
[
{
"description": "一个简单的文本接口示例",
"request": {
"uri": "/hello",
"method": "get"
},
"response": {
"text": "Hello, Moco!",
"status": 200
}
}
]
访问 http://localhost:12306/hello将返回 “Hello, Moco!”。
- 返回 JSON 数据的接口
RESTful API 常用 JSON 格式返回数据。
[
{
"description": "获取用户信息的接口",
"request": {
"uri": "/user/1",
"method": "get"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"json": {
"id": 1,
"name": "Test User",
"email": "test@example.com"
}
}
}
]
访问 http://localhost:12306/user/1将返回指定的 JSON 对象。
- 处理查询参数 (Query Parameters)
根据 URL 中的查询参数返回不同内容。
[
{
"description": "根据查询参数返回不同内容",
"request": {
"uri": "/greet",
"queries": {
"name": "zhangsan"
}
},
"response": {
"text": "Hello, zhangsan"
}
}
]
访问 http://localhost:12306/greet?name=zhangsan将返回 “Hello, zhangsan”
- 处理 POST 请求与 JSON 请求体
模拟接收 JSON 请求体的 POST 接口,例如创建资源。
[
{
"description": "创建用户的POST接口",
"request": {
"uri": "/user",
"method": "post",
"json": {
"name": "new_user"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"json": {
"id": 1001,
"name": "new_user",
"message": "User created successfully."
}
}
}
]
向 http://localhost:12306/user发送 POST 请求并携带 JSON 请求体 {“name”: “new_user”}将触发此响应。
- 处理表单数据 (Form Data)
模拟接收表单提交的接口。
[
{
"description": "处理表单登录的接口",
"request": {
"uri": "/login",
"method": "post",
"forms": {
"username": "admin",
"password": "123456"
}
},
"response": {
"headers": {
"Content-Type": "application/json"
},
"json": {
"msg": "登录成功"
},
"status": 200
}
}
]
向 http://localhost:12306/login发送表单数据 username=admin&password=123456将返回登录成功消息。
- 设置响应头与状态码
模拟特定的 HTTP 状态码和响应头,如认证失败、自定义 Header 等。
[
{
"description": "模拟需要特定Header的请求或返回自定义Header",
"request": {
"uri": "/api/data",
"method": "get",
"headers": {
"Authorization": "Bearer your_token_here"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json",
"X-Custom-Header": "CustomValue"
},
"json": {
"data": ["item1", "item2"]
}
}
},
{
"description": "模拟认证失败",
"request": {
"uri": "/api/protected"
},
"response": {
"status": 401,
"text": "Unauthorized"
}
}
]
- 模拟延迟响应 (Latency)
测试网络延迟或超时处理时非常有用。
[
{
"description": "模拟一个延迟3秒的接口",
"request": {
"uri": "/api/slow"
},
"response": {
"latency": {
"duration": 3000,
"unit": "ms"
},
"text": "This response is delayed."
}
}
]
访问 http://localhost:12306/api/slow将会等待 3 秒后才收到响应。
- 模拟异常和错误
模拟服务器内部错误(500)、找不到资源(404)等异常情况。
[
{
"description": "模拟服务器内部错误",
"request": {
"uri": "/api/error"
},
"response": {
"status": 500,
"text": "Internal Server Error"
}
}
]
访问 http://localhost:12306/api/error将返回 500 状态码和错误信息。
- 重定向 (Redirect)
模拟重定向响应。
[
{
"description": "模拟重定向到百度",
"request": {
"uri": "/redirect",
"method": "get"
},
"redirectTo": "http://www.baidu.com"
}
]
访问 http://localhost:12306/redirect将会自动跳转到百度首页。
- 从文件加载响应内容
对于大量或复杂的响应内容,可以将其放在单独的文件中引用。
[
{
"description": "从文件加载响应",
"request": {
"uri": "/large-data"
},
"response": {
"file": "path/to/your/large-data-response.json"
}
}
]
Moco 会将指定文件的内容作为响应体返回。
⚙️ 四、高级用法与技巧
- 条件匹配与动态响应
Moco 支持更复杂的条件匹配,例如仅当 JSON 请求体中特定字段满足条件时才匹配。
[
{
"description": "根据JSON请求体中的type字段返回不同响应",
"request": {
"uri": "/api/users",
"method": "post",
"json": {
"type": "admin"
}
},
"response": {
"json": {"message": "管理员创建成功"}
}
},
{
"description": "根据JSON请求体中的type字段返回不同响应",
"request": {
"uri": "/api/users",
"method": "post",
"json": {
"type": "user"
}
},
"response": {
"json": {"message": "普通用户创建成功"}
}
}
]
此配置根据请求体中 type字段的值(”admin” 或 “user”)返回不同的成功消息。
- 使用 Cookie
模拟依赖 Cookie 的请求或设置 Cookie 的响应。
[
{
"description": "请求需要携带特定Cookie",
"request": {
"uri": "/dashboard",
"cookies": {
"sessionId": "abc123"
}
},
"response": {
"text": "Welcome to dashboard!"
}
},
{
"description": "响应设置Cookie",
"request": {
"uri": "/login"
},
"response": {
"cookies": {
"sessionId": {
"value": "new_session_id_789",
"domain": "localhost",
"path": "/",
"maxAge": 3600
}
},
"text": "Login successful, session set."
}
}
]
- 在单元测试中使用 (Java API)
除了 JSON 配置,Moco 还可以直接在 Java 代码中配置,非常适合集成到 testng 单元测试中
package com.mock;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import okhttp3.*;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.io.IOException;
public class UserApiTest {
private static final String BASE_URL = "http://localhost:12306";
private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();
@BeforeClass
public void setUp() {
// 这里可以添加测试前的准备代码
// 例如:确保Mock服务器已经启动
System.out.println("测试开始,确保Mock服务器运行在 " + BASE_URL);
}
@Test
public void testGetUserSuccess() throws IOException {
// 构造请求
Request request = new Request.Builder()
.url(BASE_URL + "/api/user/1")
.get()
.build();
// 执行请求
try (Response response = client.newCall(request).execute()) {
// 验证HTTP状态码
Assert.assertEquals(response.code(), 200, "HTTP状态码应为200");
// 验证响应体
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
Assert.assertEquals(jsonResponse.get("code").getAsInt(), 0, "返回码应为0");
Assert.assertEquals(jsonResponse.get("message").getAsString(), "success", "消息应为success");
// 验证数据部分
JsonObject data = jsonResponse.getAsJsonObject("data");
Assert.assertEquals(data.get("id").getAsInt(), 1, "用户ID应为1");
Assert.assertEquals(data.get("name").getAsString(), "张三", "用户名应为张三");
Assert.assertTrue(data.get("email").getAsString().contains("@"), "邮箱应包含@符号");
}
}
@Test
public void testGetUserNotFound() throws IOException {
Request request = new Request.Builder()
.url(BASE_URL + "/api/user/999")
.get()
.build();
try (Response response = client.newCall(request).execute()) {
Assert.assertEquals(response.code(), 404, "HTTP状态码应为404");
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
Assert.assertEquals(jsonResponse.get("code").getAsInt(), 404, "错误码应为40401");
Assert.assertEquals(jsonResponse.get("message").getAsString(), "用户不存在", "错误消息应为'用户不存在'");
}
}
@Test
public void testCreateUser() throws IOException {
// 构造请求体
String json = "{\"name\": \"李四\", \"email\": \"lisi@example.com\"}";
RequestBody body = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
// 构造请求
Request request = new Request.Builder()
.url(BASE_URL + "/api/user")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
Assert.assertEquals(response.code(), 201, "HTTP状态码应为201");
// 验证Location头部
String locationHeader = response.header("Location");
Assert.assertEquals(locationHeader, "/api/user/2", "Location头部应指向新创建的用户");
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
Assert.assertEquals(jsonResponse.get("code").getAsInt(), 0, "返回码应为0");
Assert.assertEquals(jsonResponse.get("message").getAsString(), "用户创建成功", "消息应为'用户创建成功'");
// 验证返回的数据
JsonObject data = jsonResponse.getAsJsonObject("data");
Assert.assertEquals(data.get("name").getAsString(), "李四", "用户名应为李四");
Assert.assertEquals(data.get("email").getAsString(), "lisi@example.com", "邮箱应匹配");
}
}
@Test
public void testGetUsersWithQueryParams() throws IOException {
Request request = new Request.Builder()
.url(BASE_URL + "/api/users?page=1&limit=10")
.get()
.build();
try (Response response = client.newCall(request).execute()) {
Assert.assertEquals(response.code(), 200, "HTTP状态码应为200");
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
Assert.assertEquals(jsonResponse.get("code").getAsInt(), 0, "返回码应为0");
// 验证分页数据
JsonObject data = jsonResponse.getAsJsonObject("data");
Assert.assertEquals(data.get("page").getAsInt(), 1, "页码应为1");
Assert.assertEquals(data.get("limit").getAsInt(), 10, "每页限制应为10");
Assert.assertEquals(data.get("total").getAsInt(), 2, "总数应为2");
// 验证列表数据
Assert.assertTrue(data.getAsJsonArray("list").size() > 0, "用户列表不应为空");
}
}
@Test
public void testFormLogin() throws IOException {
// 构造请求体
FormBody formBody = new FormBody.Builder().add("username", "admin").add("password", "123456").build();
// 构造请求
Request request = new Request.Builder()
.url(BASE_URL + "/api/formLogin")
.post(formBody)
.build();
try (Response response = client.newCall(request).execute()) {
Assert.assertEquals(response.code(), 200, "HTTP状态码应为200");
// 验证Location头部
String locationHeader = response.header("Location");
Assert.assertEquals(locationHeader, "/api/formLogin", "Location头部");
String responseBody = response.body().string();
JsonObject jsonResponse = gson.fromJson(responseBody, JsonObject.class);
Assert.assertTrue(responseBody.contains("\"msg\":\"登录成功\""), "未返回成功消息");
String msg = jsonResponse.get("msg").getAsString();
Assert.assertEquals(msg, "登录成功", "消息应为'登录成功'");
}
}
@Test
public void testSlow() throws IOException {
// 构造请求
Request request = new Request.Builder()
.url(BASE_URL + "/api/slow")
.get()
.build();
try (Response response = client.newCall(request).execute()) {
Assert.assertEquals(response.code(), 200, "HTTP状态码应为200");
String responseBody = response.body().string();
Assert.assertTrue(responseBody.contains("This response is delayed"), "未返回成功消息");
}
}
@AfterClass
public void tearDown() {
// 这里可以添加测试后的清理代码
System.out.println("测试完成");
}
}
}
4. 管理多个配置文件
当接口很多时,可以将配置拆分到多个 JSON 文件中,然后通过一个主配置文件引入。
主配置文件 (config.json):
[
{“include”: “api-users.json”},
{“include”: “api-products.json”},
{“include”: “common-settings.json”}
]
启动命令:
使用 -g参数指定主配置文件
java -jar moco-runner-1.5.0-standalone.jar http -p 12306 -g config.json
💡 五、最佳实践建议
配置与代码分离:将接口配置写在 JSON 文件中,与代码分离,更易于管理和维护
版本化管理:将 JSON 配置文件纳入 Git 等版本控制系统,方便团队协作和追溯变更
环境区分:可以为开发、测试、预发布等不同环境准备不同的配置文件,管理各自的 Mock 规则
命名清晰:使用 description字段为每个接口配置添加清晰描述,便于后期维护
结合 CI/CD:在持续集成流水线中,可以在执行集成测试前启动 Moco 服务,提供稳定的 Mock 环境
[
{
"description": "获取用户信息 - 成功用例",
"request": {
"uri": "/api/user/1",
"method": "get"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"json": {
"code": 0,
"message": "success",
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}
}
}
},
{
"description": "获取用户信息 - 用户不存在",
"request": {
"uri": "/api/user/999",
"method": "get"
},
"response": {
"status": 404,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"json": {
"code": 404,
"message": "用户不存在",
"data": null
}
}
},
{
"description": "创建新用户",
"request": {
"uri": "/api/user",
"method": "post",
"headers": {
"Content-Type": "application/json"
},
"json": {
"name": "李四",
"email": "lisi@example.com"
}
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Location": "/api/user/2"
},
"json": {
"code": 0,
"message": "用户创建成功",
"data": {
"id": 2,
"name": "李四",
"email": "lisi@example.com"
}
}
}
},
{
"description": "获取用户列表(带查询参数)",
"request": {
"uri": "/api/users",
"method": "get",
"queries": {
"page": "1",
"limit": "10"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"json": {
"code": 0,
"message": "success",
"data": {
"list": [
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
},
{
"id": 2,
"name": "李四",
"email": "lisi@example.com"
}
],
"total": 2,
"page": 1,
"limit": 10
}
}
}
}
]