GitHub - ifermo/mockserver: Based on a predefined HTTP mock server · GitHub
Skip to content

About

Based on a predefined HTTP mock server

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

MockServer

基于 Rust 与 axum 实现的 HTTP Mock 服务。程序从 JSON 规格文件中加载请求匹配规则,在运行时根据方法、路径、Header、Body 进行顺序匹配,并返回预先定义的响应内容。

这个仓库当前更偏向“轻量可控的本地 Mock 工具”,实现简单,便于直接阅读、扩展和嵌入测试流程。

功能概览

  • 基于 axum 0.8 提供 HTTP 服务
  • 从 JSON 文件加载 Mock 规则
  • 支持路径参数模式,例如 /api/users/:id
  • 支持正则路径模式,例如 /api/products/[0-9]+
  • 支持按 Header 与 JSON Body 精确匹配
  • 支持配置响应状态码、Header、Body、延迟
  • 支持通过 SIGHUP 热重载规格文件
  • 支持 SIGTERM 优雅停机
  • 内置单元测试与集成测试

代码实现分析

1. 启动流程

程序入口位于 src/main.rs。

启动流程如下:

  1. 初始化 tracing_subscriber 日志系统
  2. 从 config.toml 读取配置;如果文件不存在则回退到默认配置
  3. 根据配置中的 spec_path 加载规格文件
  4. 将规格编译为带正则的内部结构并放入共享存储
  5. 启动 SIGHUP 监听任务,支持热重载
  6. 创建 axum Router,监听配置中的地址并开始服务
  7. 收到 SIGTERM 时执行优雅关闭

2. 配置加载

配置逻辑位于 src/config.rs。

当前支持的配置项:

  • server_addr:监听地址,默认 0.0.0.0:3000
  • body_limit:请求体读取上限,默认 65536
  • spec_path:规格文件路径,默认 spec.json

环境变量优先级:

  • MOCK_CONFIG:覆盖默认配置文件路径
  • MOCK_SPEC:覆盖配置文件中的 spec_path

这意味着即使仓库根目录没有 config.toml,程序也仍会用默认值启动;但如果默认的 spec.json 不存在,初始化规格存储时会报错退出。

3. 路由与请求处理

请求处理位于 src/app.rs。

当前路由非常直接:

  • /
  • /{*path}

所有 HTTP 方法都会进入统一的 handle_request 流程:

  1. 提取方法与路径
  2. 读取所有请求头到 HashMap<String, String>
  3. 按 body_limit 读取请求体
  4. 尝试把请求体解析为 JSON
  5. 调用 spec::match_request 在内存中的规格列表里顺序匹配
  6. 如果命中则返回配置响应;否则返回 404 {"error":"no matching spec"}

4. Spec 编译机制

Spec 编译位于 src/spec/compiler.rs。

项目不会在每次请求时重新解释路径规则,而是在加载时先把路径编译成正则:

  • 普通路径如 /api/users 会被转成精确匹配正则
  • :param 形式会转成带参数名的命名捕获组
  • 包含 [、(、\ 的路径会被当成正则表达式直接使用

编译后的运行时结构是 SpecWithPattern:

  • 原始 Spec
  • 预编译 Regex

这样请求阶段只需要执行正则匹配,不必重复做路径转换。

5. 匹配逻辑

匹配逻辑位于 src/spec/matcher.rs。

当前匹配顺序是:

  1. HTTP 方法完全相等
  2. 路径正则匹配
  3. Header 全量子集匹配
  4. Body 匹配

几个关键实现细节:

  • 匹配是“按 Spec 声明顺序优先”,找到第一个命中的规则即停止
  • Header 名按大小写无关方式匹配,例如 Content-Type 与 content-type 会被视为同一个键
  • 请求体会先被解析成 JSON,再做比较;因此当前实现更适合匹配 JSON 请求
  • Body 中字符串值 "*" 或空字符串 "" 被视为通配
  • 响应延迟在命中后通过 tokio::time::sleep 实现

6. 规格存储与热重载

规格存储位于 src/spec/store.rs。

共享存储类型是:

pub type SharedSpecStore = Arc<RwLock<Vec<SpecWithPattern>>>;

设计含义:

  • 读取请求时走读锁,开销较低
  • 热重载时重新编译整个 Spec 列表,再一次性替换
  • 重新加载失败时保留旧配置,不会把当前服务状态清空

7. 测试结构

测试分成两类:

  • 源码内单元测试:覆盖编译器、存储、基础匹配
  • tests/spec_tests.rs:覆盖完整请求路径、404、延迟、热更新等行为

仓库还提供了 integration.sh 作为基于 curl 的集成验证脚本。

8. 当前实现边界

这部分在使用前值得先看清:

  • 当前请求体匹配本质上是 JSON 匹配,不是通用文本匹配
  • Header 名按 HTTP 语义做大小写无关匹配
  • 路径参数可以通过 {{param}} 形式注入响应体模板
  • 没有请求转发、模板渲染、场景状态机等高级能力
  • SIGHUP/SIGTERM 使用 tokio::signal::unix,更适合类 Unix 环境

安装与构建

环境要求

  • Rust stable 工具链
  • Cargo
  • 建议在 macOS 或 Linux 上运行

本地构建

cargo build

或构建发布版:

cargo build --release

代码格式化与静态检查

cargo fmt
cargo clippy --all-targets

详细使用说明

这一节按“从零启动一个可用的 Mock 服务”来写,直接对应当前仓库实现。

1. 准备配置文件

程序默认读取根目录下的 config.toml。如果没有这个文件,会自动使用默认配置:

server_addr = "0.0.0.0:3000"
body_limit = 65536
spec_path = "spec.json"

你可以直接新建一个最小配置文件:

server_addr = "127.0.0.1:3000"
body_limit = 65536
spec_path = "spec.json"

如果你不想创建新文件,也可以直接使用仓库中的测试配置:

MOCK_CONFIG=test-config.toml MOCK_SPEC=test-spec.json cargo run

2. 编写 Spec 文件

Spec 文件是一个 JSON 数组,每一项定义一条请求规则和对应响应。

最小可运行示例:

[
  {
    "name": "get-user",
    "httpRequest": {
      "method": "GET",
      "path": "/api/users/:id"
    },
    "httpResponse": {
      "statusCode": 200,
      "headers": {
        "content-type": "application/json"
      },
      "body": "{\"id\":\"{{id}}\",\"name\":\"zhangsan\"}"
    }
  },
  {
    "name": "login-success",
    "httpRequest": {
      "method": "POST",
      "path": "/api/login",
      "body": {
        "username": "admin",
        "password": "123456"
      }
    },
    "httpResponse": {
      "statusCode": 200,
      "headers": {
        "content-type": "application/json"
      },
      "body": "{\"token\":\"fake-jwt-token\"}"
    }
  },
  {
    "name": "login-failed",
    "httpRequest": {
      "method": "POST",
      "path": "/api/login"
    },
    "httpResponse": {
      "statusCode": 401,
      "headers": {
        "content-type": "application/json"
      },
      "body": "{\"error\":\"invalid credentials\"}"
    }
  }
]

建议:

  • name 用于日志与调试,保持唯一且可读
  • Header 名支持大小写混写,但建议团队内部保持一致风格
  • 如果需要兜底规则,把它放在更具体规则后面

3. 启动服务

使用默认配置启动:

cargo run

使用自定义配置启动:

MOCK_CONFIG=./config.toml cargo run

只覆盖 Spec 路径:

MOCK_SPEC=./spec.json cargo run

发布版启动:

./target/release/mockserver

4. 发送请求验证

例 1:路径参数匹配

curl http://127.0.0.1:3000/api/users/100

预期返回:

{"id":"100","name":"zhangsan"}

这里的 100 来自请求路径 /api/users/100 中的 :id,通过响应体模板里的 {{id}} 注入。

例 2:JSON Body 匹配

curl -X POST http://127.0.0.1:3000/api/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"123456"}'

预期返回:

{"token":"fake-jwt-token"}

例 3:未命中规则

curl -i http://127.0.0.1:3000/not-found

预期返回状态码 404,响应体为:

{"error":"no matching spec"}

5. 使用正则路径

如果你希望按正则匹配路径,可以在 path 中直接写正则模式:

{
  "name": "get-product",
  "httpRequest": {
    "method": "GET",
    "path": "/api/products/[0-9]+"
  },
  "httpResponse": {
    "statusCode": 200,
    "body": "{\"id\":1,\"name\":\"product\"}"
  }
}

这样 /api/products/1、/api/products/999 都会命中。

6. 配置响应延迟

延迟配置写在 httpResponse.delay:

{
  "timeUnit": "SECONDS",
  "value": 2
}

支持的时间单位:

  • MILLISECONDS
  • SECONDS
  • MINUTES

示例:

{
  "name": "slow-product-api",
  "httpRequest": {
    "method": "GET",
    "path": "/api/products/[0-9]+"
  },
  "httpResponse": {
    "statusCode": 200,
    "body": "{\"ok\":true}",
    "delay": {
      "timeUnit": "SECONDS",
      "value": 2
    }
  }
}

7. 热重载 Spec

修改 Spec 文件后,向进程发送 SIGHUP:

kill -HUP <pid>

建议流程:

  1. 先修改 spec.json
  2. 用 JSON 校验工具确认格式无误
  3. 执行 kill -HUP <pid>
  4. 再次用 curl 验证新规则

如果重载失败,程序会记录错误日志,但会继续使用旧的已加载规则。

8. 查看日志

默认日志级别使用 RUST_LOG 控制;未设置时为 info。

示例:

RUST_LOG=debug cargo run

你可以看到:

  • 启动日志
  • 配置加载日志
  • 请求匹配日志
  • 热重载成功或失败日志

9. 停止服务

向进程发送 SIGTERM 可触发优雅停机:

kill -TERM <pid>

在开发环境中,Ctrl+C 是否按预期退出取决于外层运行方式;当前代码显式处理的是 SIGTERM。

配置文件说明

config.toml 示例:

server_addr = "0.0.0.0:3000"
body_limit = 65536
spec_path = "spec.json"

字段说明:

字段 类型 默认值 说明
server_addr String 0.0.0.0:3000 服务监听地址
body_limit usize 65536 请求体读取上限
spec_path String spec.json Spec 文件路径

环境变量覆盖关系:

环境变量 作用
MOCK_CONFIG 指定配置文件路径
MOCK_SPEC 覆盖 spec_path

Spec 格式说明

单条 Spec 结构:

{
  "name": "rule-name",
  "httpRequest": {
    "method": "GET",
    "path": "/api/users/:id",
    "headers": {
      "content-type": "application/json"
    },
    "body": {
      "id": 1
    }
  },
  "httpResponse": {
    "statusCode": 200,
    "headers": {
      "content-type": "application/json"
    },
    "body": "{\"ok\":true}",
    "delay": {
      "timeUnit": "SECONDS",
      "value": 1
    }
  }
}

字段说明:

  • name:规则名称
  • httpRequest.method:请求方法,当前是精确匹配
  • httpRequest.path:支持普通路径、路径参数、正则
  • httpRequest.headers:可选,定义后必须全部匹配
  • httpRequest.body:可选,适用于 JSON 匹配
  • httpResponse.statusCode:响应状态码
  • httpResponse.headers:可选,响应头
  • httpResponse.body:响应体字符串,支持使用 {{param}} 引用路径参数
  • httpResponse.delay:可选,响应延迟

匹配优先级:

  • 规则列表从前到后
  • 更具体的规则应放在前面
  • 更宽泛的兜底规则应放在后面

测试与调试

运行全部测试:

cargo test

运行带日志测试:

RUST_LOG=debug cargo test -- --nocapture

运行单个集成测试:

cargo test test_normal_matching -- --nocapture

使用脚本做端到端验证:

  1. 先启动服务
  2. 再执行:
chmod +x integration.sh
./integration.sh

Docker 说明

仓库提供了 Dockerfile,但它依赖自定义基础镜像 rust-builder:1.0.0。如果你的环境没有这个镜像,需要先自行准备,或者按你的 CI/CD 规范改成公开可用的 Rust 基础镜像。

此外,容器运行时还需要把实际使用的 config.toml 与 Spec 文件一起提供进去,否则程序仍会因为找不到默认 spec.json 而无法启动。

项目结构

.
├── src
│   ├── main.rs
│   ├── app.rs
│   ├── config.rs
│   ├── lib.rs
│   └── spec
│       ├── mod.rs
│       ├── compiler.rs
│       ├── matcher.rs
│       ├── models.rs
│       └── store.rs
├── tests
│   └── spec_tests.rs
├── test-config.toml
├── test-spec.json
├── integration.sh
└── README.md

后续可扩展方向

  • 支持文本请求体与 Content-Type 分支匹配
  • 支持自动文件监听热重载,而不是手动发送 SIGHUP
  • 支持场景状态、优先级、次数限制等更完整的 Mock 能力

About

Based on a predefined HTTP mock server

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages