moco框架


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 或更高版本以获得更好的兼容性。

  1. 直接使用独立 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参数指定主配置文件

  1. 通过构建工具引入依赖(用于集成到项目测试)
    如果你希望将 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

  1. 简单的文本接口
    这是一个最基本的 GET 请求示例,返回纯文本。
[
  {
    "description": "一个简单的文本接口示例",
    "request": {
      "uri": "/hello",
      "method": "get"
    },
    "response": {
      "text": "Hello, Moco!",
      "status": 200
    }
  }
]

访问 http://localhost:12306/hello将返回 “Hello, Moco!”。

  1. 返回 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 对象。

  1. 处理查询参数 (Query Parameters)
    根据 URL 中的查询参数返回不同内容。
[
  {
    "description": "根据查询参数返回不同内容",
    "request": {
      "uri": "/greet",
      "queries": {
        "name": "zhangsan"
      }
    },
    "response": {
      "text": "Hello, zhangsan"
    }
  }
]

访问 http://localhost:12306/greet?name=zhangsan将返回 “Hello, zhangsan”

  1. 处理 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”}将触发此响应。

  1. 处理表单数据 (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将返回登录成功消息。

  1. 设置响应头与状态码
    模拟特定的 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"
    }
  }
]
  1. 模拟延迟响应 (Latency)
    测试网络延迟或超时处理时非常有用。
[
  {
    "description": "模拟一个延迟3秒的接口",
    "request": {
      "uri": "/api/slow"
    },
    "response": {
      "latency": {
        "duration": 3000,
        "unit": "ms"
      },
      "text": "This response is delayed."
    }
  }
]

访问 http://localhost:12306/api/slow将会等待 3 秒后才收到响应。

  1. 模拟异常和错误
    模拟服务器内部错误(500)、找不到资源(404)等异常情况。
[
  {
    "description": "模拟服务器内部错误",
    "request": {
      "uri": "/api/error"
    },
    "response": {
      "status": 500,
      "text": "Internal Server Error"
    }
  }
]

访问 http://localhost:12306/api/error将返回 500 状态码和错误信息。

  1. 重定向 (Redirect)
    模拟重定向响应。
[
  {
    "description": "模拟重定向到百度",
    "request": {
      "uri": "/redirect",
      "method": "get"
    },
    "redirectTo": "http://www.baidu.com"
  }
]

访问 http://localhost:12306/redirect将会自动跳转到百度首页。

  1. 从文件加载响应内容
    对于大量或复杂的响应内容,可以将其放在单独的文件中引用。
[
  {
    "description": "从文件加载响应",
    "request": {
      "uri": "/large-data"
    },
    "response": {
      "file": "path/to/your/large-data-response.json"
    }
  }
]

Moco 会将指定文件的内容作为响应体返回。

⚙️ 四、高级用法与技巧

  1. 条件匹配与动态响应
    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”)返回不同的成功消息。

  1. 使用 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."
    }
  }
]
  1. 在单元测试中使用 (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
        }
      }
    }
  }
]

文章作者: 读序
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 读序 !
  目录