新手区 基于 mock-server 实现 mock 平台

cheunghr · 2021年04月21日 · 最后由 cheunghr 回复于 2024年09月27日 · 5790 次阅读

基于 mock-server 实现 mock 平台

本篇主要说明一些主要实现思路,不会放太多代码

主要基于 mock-sever,官方地址https://www.mock-server.com/
maven

<dependency>
    <groupId>org.mock-server</groupId>
    <artifactId>mockserver-netty</artifactId>
    <version>5.11.1</version>
</dependency>

数据库表结构


CREATE TABLE `t_mock_api` (
  `api_id` int NOT NULL AUTO_INCREMENT,
  `server_id` int DEFAULT NULL,
  `desc` varchar(30) DEFAULT NULL COMMENT '描述',
  `url` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求地址',
  `method` varchar(20) DEFAULT NULL COMMENT '请求方式',
  `response_code` int DEFAULT NULL COMMENT '状态码',
  `response_headers` varchar(1000) DEFAULT NULL COMMENT '响应头',
  `response_body` mediumtext COMMENT '响应body',
  `response_delay` int DEFAULT NULL COMMENT '响应时延ms',
  `status` tinyint DEFAULT NULL COMMENT '0启用1禁用',
  `created_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `response_headers_enable_rely` tinyint DEFAULT NULL COMMENT '0是1否 是否解析headers依赖',
  `response_body_enable_rely` tinyint DEFAULT NULL COMMENT '0是1否 是否解析body依赖',
  `creator_id` int DEFAULT NULL COMMENT '创建人id',
  `creator_name` varchar(30) DEFAULT NULL COMMENT '创建人名称',
  `response_body_type` tinyint DEFAULT NULL COMMENT '0文本1json2xml3html',
  PRIMARY KEY (`api_id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;


CREATE TABLE `t_mock_hit_policy` (
  `id` int NOT NULL AUTO_INCREMENT,
  `api_id` int DEFAULT NULL COMMENT 'api_id',
  `match_scope` tinyint DEFAULT NULL COMMENT '0请求头1请求body2pathparams3请求queryparams',
  `match_type` tinyint DEFAULT NULL COMMENT '0固定值1包含2正则3jsonschema4xpath5jsonpath',
  `name` varchar(200) DEFAULT NULL COMMENT '名称',
  `value` varchar(200) DEFAULT NULL COMMENT '值',
  `status` tinyint DEFAULT NULL COMMENT '0启用1禁用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8;


CREATE TABLE `t_mock_sever` (
  `server_id` int NOT NULL AUTO_INCREMENT,
  `port` int DEFAULT NULL COMMENT '端口号',
  `remote_host` varchar(30) DEFAULT NULL COMMENT '当没有命中时转发的主机地址',
  `remote_port` int DEFAULT NULL COMMENT '当没有命中时转发的端口',
  `desc` varchar(200) DEFAULT NULL COMMENT '描述',
  `creator_id` int DEFAULT NULL COMMENT '创建人user_id',
  `creator_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '创建人realname',
  `created_time` datetime DEFAULT NULL COMMENT 'update_time',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`server_id`),
  UNIQUE KEY `port` (`port`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

1.主要功能

  • 多节点:可满足不同人员对于相同接口的配置
  • 动态注入:运行时注入/启停 mock-server、mock-api,无需启停服务
  • 可视化报告:由 mock-server-ui 提供
  • 丰富的命中策略:支持 header、body、queryparams、pathparams
  • 支持自动转发:在未命中 mock-api 时支持自动转发真实服务
  • 支持响应延时配置

2.实现方式

2.1、多节点

多节点即指在同一台服务器上开启多台 mock-server,避免人员之间交叉维护同一个接口,主要的实现方式是类似一个 “池” 的概念,有即直接取,无即创建。

  • 第一步:维护一个单例,用来 “装” mock-server,我使用的是一个 HashMap,以端口号为 key,以 mocksever 为 value。贴下核心代码,据说用 String 作为 key 性能更好点,hahaha
private static class SinglePool {
    private static final HashMap<String, ClientAndServer> INSTANCE = new HashMap<>(16);
}

public static HashMap<String, ClientAndServer> getInstance() {
    return SinglePool.INSTANCE;
}
  • 第二步:封装常用 mock-server 方法。核心思想:在操作 mock-sever 的时候同时维护节点池
/**
 * 启动MockServer
 * @param port 端口
 * @return ClientAndServer
 * @throws BusinessException 端口被占用
 */
public static ClientAndServer start(Integer port) throws BusinessException {
    if (port == null) {
        throw new BusinessException("端口号不能为空");
    }
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    ClientAndServer mockServerClient;
    try {
        mockServerClient = new ClientAndServer(port);
    } catch (Exception e) {
        throw new BusinessException("启动失败,请更换端口重试");
    }
    instance.put(port.toString(), mockServerClient);
    return mockServerClient;
}


/**
 * 判断端口是否已启用MockServer
 * @param port 端口
 * @return boolean
 */
public static boolean isRunning(Integer port) {
    if (port == null) {
        throw new IllegalArgumentException("端口号不能为空");
    }
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    ClientAndServer hit = instance.get(port.toString());
    if (hit == null) {
        return false;
    } else {
        return hit.getPort().equals(port);
    }
}

/**
 * 停止指定端口的MockServer
 * @param port 端口
 */
public static void stop(Integer port) {
    if (port == null) {
        throw new IllegalArgumentException("端口号不能为空");
    }
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    ClientAndServer hit = instance.get(port.toString());
    if (hit != null) {
        if (isRunning(port)) {
            hit.stop(true);
            instance.remove(port.toString());
        }
    }
}

/**
 * 获取一个mock sever, 端口已经启动则直接返回;否则创建再返回
 * @param port 端口
 * @return ClientAndServer
 * @throws BusinessException 端口被占用
 */
public static ClientAndServer get(Integer port) throws BusinessException {
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    if (isRunning(port)) {
        ClientAndServer hit = instance.get(port.toString());
        InetSocketAddress remoteAddress = hit.getRemoteAddress();
        if (remoteAddress == null) {
            return hit;
        } else {
            hit.stop(true);
            return start(port);
        }
    } else {
        return start(port);
    }
}

public static ClientAndServer justGet(Integer port) throws BusinessException {
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    ClientAndServer server = instance.get(String.valueOf(port));
    if (server == null) {
        throw new BusinessException("获取mock server失败");
    } else {
        return server;
    }
}

2.2、动态注入

根据前端的不同操作,后端做出不同响应

  • 新增:根据绑定的 mock-server 获取其端口号,从节点池获取 ClientAndServer,仅 ClientAndServer 为运行时,才做新增和注入 api 操作;否则只新增数据,无需向 mock-server 注入一个 api(因为它未运行)

  • 修改:修改分为三个步骤:删除修改之前 mock-api、修改数据库数据、启动修改后的 mock-api。

    • 如何删除数据库 mock-api 的记录时同时关闭 mock-server 中的 mock-api 呢?由于我的 path 做了唯一性校验,因此我会将 mock-server 中相同 url 的 mock-api 删除
/**
 * 清空mock server 下指定路径的api
 * @param port 指定端口
 * @param path path
 */
public static void clearByPath(Integer port, String path) {
    if (isRunning(port)) {
        HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
        ClientAndServer clientAndServer = instance.get(String.valueOf(port));
        if (clientAndServer != null) {
            clientAndServer.clear(HttpRequest.request().withPath(path));
        }
    }
}

2.3、可视化报告

mock-server 提供了报告,其访问地址为http://host:port/mockserver/dashboard

2.4、命中策略

mock-server 提供了很多种命中策略,我只对一些常用的进行了封装 header、body、queryparams、pathparams。这一块可以去官网查看

2.5、自动转发

在创建 ClientAndServer 对象时,提供了两种构造方法。第二个构造方法,Integer remotePort, Integer... ports,可配置在未命中 mock-api 时支持自动转发真实服务。

public ClientAndServer(Integer... ports) {
    super(new CompletableFuture());
    this.mockServer = new MockServer(ports);
    this.completePortFutureAndOpenUI();
}

public ClientAndServer(String remoteHost, Integer remotePort, Integer... ports) {
    super(new CompletableFuture());
    this.mockServer = new MockServer(remotePort, remoteHost, ports);
    this.completePortFutureAndOpenUI();
}

2.6、响应延时

HttpResponse.withDelay(TimeUnit.MILLISECONDS, ms) // 可指定接口响应延时

3.实现效果

节点管理

api 管理



运行

可以看出,mock-server 以及 mock-api 都存在状态,而数据库表结构未存在状态字段,主要是由于状态存在与内存,那么如何判断 mock-server 以及 mock-api 是启动状态的呢?
获取 mock-server 状态

public PageInfo<MockServerVO> findMockServer(MockServerDTO mockServerDTO, Integer pageNum, Integer pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    PageInfo<MockServerVO> pages = new PageInfo<>(mockServerMapper.selectMockServer(mockServerDTO));
    List<MockServerVO> result  = pages.getList().stream().peek(page -> {
        Integer port = page.getPort();
        byte status = (byte) (MockServerPool.isRunning(port) ? 0 : 1);
        page.setStatus(status);
    }).collect(Collectors.toList());
    pages.setList(result);
    return pages;
}
public static boolean isRunning(Integer port) {
    if (port == null) {
        throw new IllegalArgumentException("端口号不能为空");
    }
    HashMap<String, ClientAndServer> instance = MockServerPool.getInstance();
    ClientAndServer hit = instance.get(port.toString());
    if (hit == null) {
        return false;
    } else {
        return hit.getPort().equals(port);
    }
}

获取 mock-api 状态。clientAndServer.retrieveActiveExpectations,返回的数组序列化能看到目前所有运行的 api

@Override
public PageInfo<MockApiListVO> findMockApiList(MockApiDTO mockApiDTO, Integer pageNum, Integer pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    PageInfo<MockApiListVO> pages = new PageInfo<>(mockApiMapper.selectMockApiList(mockApiDTO));
    List<MockApiListVO> result  = pages.getList().stream().peek(page -> {
        Integer port = page.getPort();
        Integer apiId = page.getApiId();
        String method = page.getMethod();
        String url = page.getUrl();
        List<MockHitPolicyVO> policies = mockHitPolicyService.findMockHitPolicyByApiId(apiId);
        boolean running = MockServerPool.isRunning(port);
        page.setPortRunning(running);
        try {
            HttpRequest httpRequest = injectionCenter.injectRequest(new HttpRequest(), method, url, policies);
            page.setApiRunning(MockServerPool.apiIsRunning(port, httpRequest, url));
        } catch (BusinessException e) {
            page.setApiRunning(false);
        }
    }).collect(Collectors.toList());
    pages.setList(result);
    return pages;
}
public static boolean apiIsRunning(Integer port, HttpRequest request, String url) {
    ArrayList<String> pathList = new ArrayList<>();
    if (!isRunning(port)) {
        return false;
    }
    try {
        ClientAndServer clientAndServer = justGet(port);
        Expectation[] expectations = clientAndServer.retrieveActiveExpectations(request);
        JSONArray array = JSONArray.parseArray(JSON.toJSONString(expectations));
        for (int i = 0; i < array.size(); i++) {
            JSONObject jsonObject = array.getJSONObject(i);
            JSONObject httpRequest = jsonObject.getJSONObject("httpRequest");
            JSONObject path = httpRequest.getJSONObject("path");
            String pathValue = path.getString("value");
            pathList.add(pathValue);
        }
    } catch (Exception e) {
        return false;
    }
    return pathList.contains(url);
}
共收到 9 条回复 时间 点赞

你好,请问一下有源码分享吗

shixiaomu58 回复

由于与接口平台耦合在一块,单独抽出来也比较麻烦。本文旨在提供一些 mock 平台化的思路

仅楼主可见

github 源码地址能私发一下嘛,学习学习

源码呢兄弟

仅楼主可见
zhangyanyun 回复

整个接口测试平台都已经开源了呀~! 详见https://github.com/Biexei/

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册