基于数据Mock的接口治理方案设计与实现

Posted by WGrape的博客 on August 25, 2022

文章内容更新请以 WGrape GitHub博客 : 基于数据Mock的接口治理方案设计与实现 为准

前言

本文原创,著作权归WGrape所有,未经授权,严禁转载

一、背景

《关于接口文档高效治理方案的研究和思考》一文中已经详细介绍了高效管理接口文档的重要性,更多内容可以阅读原文。这里就不再解释为什么需要,而是重点关注怎么做。

在本文接下来的内容中,会为大家分享一种轻量级的创新型方案,它不但可以简单高效实现接口文档的自动生成,而且可以满足前端人员Mock接口的需要。这就是基于数据Mock的接口治理方案,请慢慢往下看,大约会花费8分钟的时间。

二、目标与调研

1、自动生成接口文档

接口治理的第一个目标就是实现自动生成接口文档。后端人员只需在开发接口的同时按照规范编写相应逻辑,即可在CI阶段自动生成接口文档。

2、满足Mock接口的需要

接口治理的第二个目标就是满足前端人员Mock接口的需要。无论是开发、联调、测试、上线后的哪一个阶段,前端都可以根据自己的需要,随时Mock后端的接口。

3、设计调研

(1) 接口文档的组成

我们知道,无论是什么类型的接口文档,其必须具备以下组成元素

  • 接口地址 :调用的接口地址
  • 接口参数 :参数名称、类型、含义
  • 接口返回 :接口的返回结构,包括每个字段的含义

不过有些信息比如参数is_new表示是否为新用户,这些信息都是属于程序无法自动获取的自定义内容,它们还是需要人力提供的,只是提供的方式可能有所不同。

比如在各大接口文档自动生成工具中,都是通过注解的方式提供,如下所示

image

(2) 接口文档的托管

一般情况下,自动生成接口文档的平台会提供私有部署方案,接口文档就会被托管在私有的服务器上。这种方式看起来很方便,但是部署和维护的成本也都是不可忽视的。能不能不依赖托管服务器呢 ?

经过调研发现Gitlab Wiki功能可以符合预期要求

  • 不需要托管服务器
  • 通过提供的API可以快速实现自动更新

三、什么是数据Mock

Mock直译为伪造,表示虚假的含义。在计算机技术中,数据Mock指通过伪造数据,实现预期目标。它最广泛的应用领域主要在接口Mock中。如下代码所示,通过返回接口虚假数据,让接口处于可用状态,方便前端调试调用

func GetUser() *model.RespBase {
    response := model.RespBase{
            Code: 0,
            Data: model.QueryGetUserResp{
            Uid:    88328876,
            Name:   "Peter",
            Age:    45,
            Gender: 1,
            City:   "New York",
        },
    }
    return &response
}

四、方案设计

1、设计思想

基于数据Mock,实现对接口的治理

2、接口文档抽象

在第二节《目标与调研》中我们分析了接口文档的组成,那么自动生成接口文档的第一步,就是需要先把接口文档抽象到程序中。

在如下程序中,定义的APIMock结构体即为一个接口的接口文档的抽象,也就是说整个接口文档会由[]APIMock组成,即多个APINock组成。

// RequestExplainItem 请求参数释义
type RequestExplainItem struct {
    Field  string `json:"field"`
    Type   string `json:"type"`
    IsMust bool   `json:"is_must"`
    Mark   string `json:"mark"`
}

// ResponseExplainItem 响应结构释义
type ResponseExplainItem struct {
    Field string `json:"field"`
    Type  string `json:"type"`
    Mark  string `json:"mark"`
}

// APIMock 接口文档抽象结构
type APIMock struct {
    Title           string                `json:"title"`
    Description     string                `json:"description"`
    Route           string                `json:"route"`
    Request         interface{}           `json:"request"`
    RequestExplain  []RequestExplainItem  `json:"request_explain"`
    Response        interface{}           `json:"response"`
    ResponseExplain []ResponseExplainItem `json:"response_explain"`
}

举个例子,如果开发了一个/api/get_user接口,那么创建的APIMock对象如下所示

apiMock := apimock.APIMock{
    Title:       "获取用户接口",
    Description: "获取用户接口",
    Route:       "/api/get_user",
    Request:     request,
    RequestExplain: []apimock.RequestExplainItem{
        {
            Field:  "uid",
            Type:   "int",
            IsMust: true,
            Mark:   "用户id",
        },
    }
    Response: response,
    ResponseExplain: []apimock.ResponseExplainItem{
        {
            Field: "data.uid",
            Type:  "int",
            Mark:  "用户ID",
        },
        {
            Field: "data.name",
            Type:  "string",
            Mark:  "用户昵称",
        },
    },
}

3、Request对象和Response对象

APIMock的对象结构体中,有一个Request对象和一个Response对象。这两个对象是对接口请求参数和响应结构的抽象。 为什么需要定义这两个对象呢,下面会从两个方面来阐述原因

(1) 作为接口定义的一部分

接口定义是指定义接口的路由,请求参数和响应结构。 在PHP等弱类型语言中,接口定义这个概念的应用不多,因为所有接口参数和响应结构用$array = array()这样的数组类型就能满足需要。但是对于Go等强类型语言来说,接口定义是一个接口的必要组成。

// QueryGetUserReq 接口请求参数结构
type QueryGetUserReq struct {
    Uid int `json:"uid"`
}

// QueryGetUserReq 接口请求响应结构
type QueryGetUserResp struct {
    Uid int `json:"uid"`
    Name string `json:"name"`
}

(2) 作为接口文档的一部分

很多情况下,接口的请求参数和响应结构都是比较复杂的嵌套结构(如下图所示)。在使用Wiki维护接口文档的时代,经常需要靠人力去编写和修改这些结构,这是一件非常不友好且极其低效率的事情。

为了解决这个问题,Request对象和Response对象的重要性在此就体现出来了,直接使用json.Marshal()编码就能获取到接口文档中的这些请求参数和响应结构。

image

4、Mock请求与非Mock请求

如果当前接口请求地址中有mock_appmock_token这两个参数且校验通过,则说明这个请求为Mock请求。

CURL -X GET 'https://test.api.com/api/get_user?mock_app=test&mock_token=avhsuekfiabs'

(1) 是Mock请求

如果当前请求是Mock请求,那么调用Mock逻辑返回Mock数据。如下所示直接New一个自定义的MockResponse对象返回即可。

func returnMockGetUser() Response {
    response := model.RespBase{
        Code: 0,
        Data: model.QueryGetUserResp{
            Uid:    88328876,
            Name:   "Peter",
        },
    }
}

(2) 不是Mock请求

如果当前请求不是Mock请求,那么执行正常业务逻辑,返回正常业务数据。

5、生成Markdown格式文档

在获取到[]APIMock结构后,通过循环遍历拼接Markdown格式的方式,即可在代码仓库下生成自定义结构的接口文档。

// 生成文档内容
for index, apiMock = range apiMockList {
    // 接口描述部分
    contentList = append(contentList, "\n### (1) Description\n")
    contentList = append(contentList, apiMock.Description+"\n")

    // 接口地址部分
    contentList = append(contentList, "\n### (2) URL\n")
    contentList = append(contentList, apiMock.Route+"\n")
}

// 写入apidoc.md文件
var content = strings.Join(contentList, "")
os.WriteFile("apidoc.md", result, 0666) 

6、自动更新至Gitlab

在本地生成自定义结构的接口文档后,把它更新至Gitlab会包括两部分,第一是指随着代码的提交而更新Gitlab仓库中的apidoc.md文件,第二是指通过Rest API更新Gitlab Wiki

这样无论是仓库中的apidoc.md文件,还是Gitlab Wiki,都可以自由的选择使用。

image

截屏2022-08-26 11 21 27

7、集成至CI

如果项目支持CI,可以非常方便的把接口文档自动生成和更新至Gitlab的这两个逻辑都集成至CI中,可以参考CIManager项目的.gitlab/apidoc_gen.sh

8、总结

简单总结下,这种设计方案主要包括4个关键点

  • 根据定义的APIMock/Request/Response等对象,巧妙利用Json解析等技术自动生成文接口档
  • 定义的Response还可以用来作为Mock接口的返回
  • 通过CI把接口文档自动更新至Gitlab
  • 参数控制是Mock请求还是真实请求

五、实现过程

本设计方案的实现过程,请实现请参考项目apimock。假设现在你有一个项目,目录结构如下所示,或直接查看原项目代码

image

其中,所有接口的基础返回结构只有code/data/is_mock三个字段

type RespBase struct {
    Code   int         `json:"code"`
    Data   interface{} `json:"data"`
    IsMock bool        `json:"is_mock"`
}

现在需要新增一个/api/get_user接口,如何在你的项目中使用此方案呢 ?

六、如何使用

1、填充Handler

首先找到Handler文件/server/handler.go,然后在接口对应的Handler中判断当前请求是否为Mock请求,如果是Mock请求,则返回Mock数据,如调用mock.GetUser()方法。否则就调用正常的业务逻辑,如调用service.GetUser(uidInt)方法。

func GetUserHandler(w http.ResponseWriter, r *http.Request) {
    var isMock = mock.IsMockRequest(r)

    uidString := r.URL.Query().Get("uid")
    uidInt, err := strconv.ParseInt(uidString, 10, 64)
    if err != nil {
        ResponseError(w, isMock, 100)
        return
    }

    if isMock {
        ResponseOk(w, isMock, mock.GetUser())
        return
    } else {
        ResponseOk(w, isMock, service.GetUser(uidInt))
        return
    }
}

2、定义接口的请求和响应结构体

在定义mock.GetUser()函数前,需要先在/model/model.go文件中定义接口的请求结构体和响应结构体,如下所示。

type QueryGetUserReq struct {
    Uid int64 `json:"uid"`
}

type QueryGetUserResp struct {
    Uid    int64  `json:"uid"`
    Name   string `json:"name"`
    Age    uint8  `json:"age"`
    Gender uint8  `json:"gender"`
    City   string `json:"city"`
}

3、定义mock.GetUser()函数

在定义完接口的请求和响应结构体后,先在根目录下创建/mock/mock.go文件,之后就可以开始按照如下步骤,在/mock/mock.go文件中定义mock.GetUser()函数了。

func GetUser() *apimock.APIMock {
    // 1、创建请求对象
    request := model.QueryGetUserReq{
        Uid: 87654321,
    }

    // 2、创建响应对象
    response := model.RespBase{
        Code: 0,
        Data: model.QueryGetUserResp{
            Uid:    88328876,
            Name:   "Peter",
            Age:    45,
            Gender: 1,
            City:   "New York",
        },
    }

    // 3、创建apiMock对象
    apiMock := apimock.APIMock{
        // 接口名称
        Title:       "获取用户接口",

        // 接口描述
        Description: "获取用户接口",

        // 接口路由
        Route:       "/api/get_user",

        // 请求对象和请求对象的字段释义
        Request:     request,
        RequestExplain: []apimock.RequestExplainItem{
            {
                Field:  "uid", // 字段名称
                Type:   "int", // 字段类型
                IsMust: true, // 是否必填
                Mark:   "用户id", // 字段备注
            },
        },

        // 响应对象和响应对象的字段释义
        Response:    response,
        ResponseExplain: []apimock.ResponseExplainItem{
            {
                Field: "data.uid",
                Type:  "int",
                Mark:  "用户ID",
            },
            {
                Field: "data.name",
                Type:  "string",
                Mark:  "用户昵称",
            },
            {
                Field: "data.age",
                Type:  "int",
                Mark:  "用户年龄",
            },
            {
                Field: "data.gender",
                Type:  "int",
                Mark:  "用户性别, 1女, 2男",
            },
            {
                Field: "data.city",
                Type:  "string",
                Mark:  "用户所在城市",
            },
        },
    }

    return &apiMock
}

4、定义TestAPIDocGen函数

mock目录下创建mock.go文件对应的/mock/mock_test.go单元测试文件,然后定义TestAPIDocGen()函数

func TestAPIDocGen(t *testing.T) {
    var apiMockList []*apimock.APIMock

    // 生成所有的apiMock,并生成Markdown格式的字符串文档
    apiMock := GetUser()
    apiMockList = append(apiMockList, apiMock)
    result, err := apimock.GenerateAPIDocument(apimock.APIDDocumentFormatMarkdown, apiMockList)
    if err != nil {
        t.Fail()
        return
    }

    // 文档写入至本地文件中
    if err = os.WriteFile("../apidoc.md", result, 0666); err != nil {
        t.Fail()
        return
    }
}

这样,使用go test时即可自动在本地生成apidoc.md接口文档了。

5、完成

恭喜,到这一步已经完成了所有需要的操作。你可以选择自己执行/mock/mock_test.go中的TestAPIDocGen()函数更新接口文档,也可以选择由CI自动完成Gitlab Wiki的更新

七、总结

本文主要设计并实现了一种基于数据Mock的接口治理方案,不但解决了接口文档自动生成的问题,而且还可以满足前端人员的Mock接口需求。值得一提的是,这种设计更是一种通用的解决方案,它不局限于Go语言,在其他语言中也可以适用,非常简洁,通过轻量的方式即可实现。