如何自动测试Golang Gin-gonic RESTful APIs

617 阅读10分钟

本教程包括:

  1. 使用Gin-gonic框架用Golang构建一个RESTful API
  2. 编写和运行端点的测试
  3. 自动化测试

Gin是一个用Golang编写的高性能HTTP网络框架。它包含路由和中间件等开箱即用的特性和功能。这有助于减少模板代码,提高生产力,并简化了构建微服务的过程。

在本教程中,我将指导你使用Gin-gonic框架用Golang构建一个RESTful API的过程。我还将引导你建立一个API来管理一个公司的基本细节。这个API将允许你创建、编辑、删除和检索公司的列表。

为了简单起见,我不会在本教程中介绍数据持久性。相反,我们将使用一个假的公司列表,我们可以相应地更新或删除。虽然听起来很简单,但这足以让你开始用Golang构建强大的API和单元测试。

前提条件

要从本教程中获得最大的收获,你将需要以下条件。

我们的教程是不分平台的,但以CircleCI为例。如果您没有CircleCI账户,请**在这里注册一个免费账户。**

开始学习

要开始,通过终端导航到你的开发文件夹,并使用以下命令为项目创建一个新的文件夹。

mkdir golang-company-api
cd golang-company-api

前面的命令创建了一个名为golang-company-api 的文件夹,并导航到它。

接下来,通过发布命令在项目中初始化一个Go模块

go mod init golang-company-api

这将创建一个go.mod 文件,其中将列出你的项目的依赖关系,以便跟踪。

安装项目的依赖项

如前所述,本项目将使用Gin框架作为外部依赖。从你的项目根部发出以下命令来安装最新版本的Gin和其他依赖项。

go get -u github.com/gin-gonic/gin github.com/stretchr/testify github.com/rs/xid

一旦安装过程成功,你就可以在你的应用程序中访问Gin和以下软件包。

  • Testify是Golang中最流行的测试包之一。
  • XID是一个全球唯一的ID生成器库。

创建主页

现在,在项目的根目录下创建一个名为main.go 的文件。这将是应用程序的入口点,也将容纳大部分负责所有功能的函数。打开这个新文件,并使用以下内容。

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func HomepageHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message":"Welcome to the Tech Company listing API with Golang"})
}


func main() {
    router := gin.Default()
    router.GET("/", HomepageHandler)
    router.Run()
}

这段代码导入了Gin和提供HTTP客户端和服务器实现的net/http包。它创建了一个HomepageHandler() 方法来处理你应用程序主页上的响应。

最后,main() 函数初始化了一个新的Gin路由器,为主页定义了HTTP动词,并通过调用Gin实例的Run() ,在默认端口8080 ,运行一个HTTP服务器。

运行该项目

运行该项目。

go run main.go

该命令在默认端口8080 上运行应用程序。转到http://localhost:8080 ,查看它。

Application homepage

现在应用程序按预期工作,你可以开始实现API端点所需的逻辑。现在,用CTRL+C停止应用程序的运行。然后按回车键

创建REST APIs

在你继续之前,你需要定义一个数据结构,用来保存公司的信息。这将包含一个公司的属性和字段。每个公司将有一个ID ,一个NameCEO ,和Revenue --该公司产生的估计年收入。

定义公司模型

使用Go结构来定义这个模型。在main.go 文件中,声明以下结构。

type Company struct {
    ID     string  `json:"id"`
    Name  string  `json:"name"`
    CEO string  `json:"ceo"`
    Revenue  string `json:"revenue"`
}

为了轻松地将每个字段映射到特定的名称,使用反斜线指定每个字段的标签。这可以让您发送符合JSON命名规则的适当响应。

定义一个全局变量

接下来,定义一个全局变量来代表公司,并用一些假数据来初始化这个变量。在main.go 文件中,就在Company 结构之后,添加。

var companies = []Company{
    {ID: "1", Name: "Dell", CEO: "Michael Dell", Revenue: "92.2 billion"},
    {ID: "2", Name: "Netflix", CEO: "Reed Hastings", Revenue: "20.2 billion"},
    {ID: "3", Name: "Microsoft", CEO: "Satya Nadella", Revenue: "320 million"},
}

创建一个新公司

接下来,定义创建新公司的必要逻辑。在main.go 文件中创建一个新方法,将其称为*NewCompanyHandler* ,并使用这段代码。

func NewCompanyHandler(c *gin.Context) {
    var newCompany Company
    if err := c.ShouldBindJSON(&newCompany); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    newCompany.ID = xid.New().String()
    companies = append(companies, newCompany)
    c.JSON(http.StatusCreated,  newCompany)
}

这段代码将传入的请求主体绑定到一个Company 结构实例中,然后指定一个唯一的ID 。它将newCompany 附加到公司列表中。如果有错误,它会返回一个错误响应,否则会返回一个成功响应。

获取公司列表

为了检索公司列表,定义一个*GetCompaniesHandler* 方法。

func GetCompaniesHandler(c *gin.Context) {
    c.JSON(http.StatusOK, companies)
}

这使用c.JSON() 方法将companies 数组映射成JSON并返回。

更新一个公司

要更新一个现有公司的详细信息,请定义一个名为*UpdateCompanyHandler* 的方法,该内容。

func UpdateCompanyHandler(c *gin.Context) {
    id := c.Param("id")
    var company Company
    if err := c.ShouldBindJSON(&company); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }
    index := -1
    for i := 0; i < len(companies); i++ {
        if companies[i].ID == id {
            index = 1
        }
    }
    if index == -1 {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "Company not found",
        })
        return
    }
    companies[index] = company
    c.JSON(http.StatusOK, company)
}

这个片段使用c.Param() 方法从请求的URL中获取公司的唯一id 。它检查该记录是否存在于公司的列表中,然后相应地更新指定的公司。

删除一个公司

用这个内容创建一个*DeleteCompanyHandler* 方法。

func DeleteCompanyHandler(c *gin.Context) {
    id := c.Param("id")
    index := -1
    for i := 0; i < len(companies); i++ {
        if companies[i].ID == id {
            index = 1
        }
    }
    if index == -1 {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "Company not found",
        })
        return
    }
    companies = append(companies[:index], companies[index+1:]...)
    c.JSON(http.StatusOK, gin.H{
        "message": "Company has been deleted",
    })
}

类似于*UpdateCompanyHandler* ,这个代码段的方法使用唯一的标识符来锁定需要从列表中删除的公司的详细信息。它删除了该公司的详细信息,并返回一个成功的响应。

设置API路由处理程序

接下来,注册所有适当的端点,并将它们映射到前面定义的方法。更新main() ,如图所示。

func main() {
    router := gin.Default()
    router.GET("/", HomepageHandler)
    router.GET("/companies", GetCompaniesHandler)
    router.POST("/company", NewCompanyHandler)
    router.PUT("/company/:id", UpdateCompanyHandler)
    router.DELETE("/company/:id", DeleteCompanyHandler)
    router.Run()
}

如果你使用的是支持自动导入包的代码编辑器或IDE,这就会被更新。如果你没有使用这种类型的编辑器或IDE,请确保import 与这个片段相匹配。

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "github.com/rs/xid"
)

测试应用程序

在定义了所需的方法和注册了各个端点之后,回到终端,使用go run main.go ,再次运行该应用程序。这将启动应用程序在端口8080

创建一个新的公司

使用Postman或你喜欢的API测试工具进行测试。发送一个HTTP POST请求到http://localhost:8080/company。使用下面的数据作为请求的有效载荷。

{
  "name":"Shrima Pizza",
  "ceo": "Demo CEO",
  "revenue":"300 million"
}

Create a new company

检索公司列表

要检索公司列表,请设置一个HTTPGET 请求到http://localhost:8080/companies

Retrieve companies

为端点编写测试

现在,你的应用程序正在按预期工作,专注于为所有为处理API端点的逻辑而创建的方法编写单元测试。

Golang在开箱时就安装了一个测试包,使编写测试更加容易。首先,创建一个名为main_test.go 的文件,并在其中加入以下内容。

package main
import "github.com/gin-gonic/gin"

func SetUpRouter() *gin.Engine{
    router := gin.Default()
    return router
}

这是一个用于返回Gin路由器实例的方法。在测试每个端点的其他功能时,它将派上用场。

注意你项目中的每个测试文件必须以_test.go 结尾,每个测试方法必须以Test 前缀开始。这是一个有效测试的标准命名惯例。

测试主页响应

main_test.go 文件中,定义一个*TestHomepageHandler* 方法并使用这个代码。

func TestHomepageHandler(t *testing.T) {
    mockResponse := `{"message":"Welcome to the Tech Company listing API with Golang"}`
    r := SetUpRouter()
    r.GET("/", HomepageHandler)
    req, _ := http.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    responseData, _ := ioutil.ReadAll(w.Body)
    assert.Equal(t, mockResponse, string(responseData))
    assert.Equal(t, http.StatusOK, w.Code)
}

这个测试脚本使用Gin引擎设置了一个服务器,并向主页/ ,发出了一个GET 的请求。然后它使用testify包中的assert 属性来检查状态代码和响应的有效载荷。

测试创建新公司端点

为了测试你的API的/company 端点,创建一个*TestNewCompanyHandler* 方法,并为它使用这段代码。

func TestNewCompanyHandler(t *testing.T) {
    r := SetUpRouter()
    r.POST("/company", NewCompanyHandler)
    companyId := xid.New().String()
    company := Company{
        ID: companyId,
        Name: "Demo Company",
        CEO: "Demo CEO",
        Revenue: "35 million",
    }
    jsonValue, _ := json.Marshal(company)
    req, _ := http.NewRequest("POST", "/company", bytes.NewBuffer(jsonValue))

    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
    assert.Equal(t, http.StatusCreated, w.Code)
}

这段代码发出一个带有样本有效载荷的POST 请求,并检查返回的响应代码是否为201 StatusCreated

测试获取公司端点

接下来是测试GET /companies 资源的方法。用这个定义*TestGetCompaniesHandler* 方法。

func TestGetCompaniesHandler(t *testing.T) {
    r := SetUpRouter()
    r.GET("/companies", GetCompaniesHandler)
    req, _ := http.NewRequest("GET", "/companies", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)

    var companies []Company
    json.Unmarshal(w.Body.Bytes(), &companies)

    assert.Equal(t, http.StatusOK, w.Code)
    assert.NotEmpty(t, companies)
}

这段代码向/companies 端点发出一个GET 请求,并确保返回的有效载荷不是空的。它还断言状态代码是200

测试更新公司的端点

最后一个测试是针对负责更新公司信息的HTTP处理程序。在main_test.go 文件中使用这个代码段。

func TestUpdateCompanyHandler(t *testing.T) {
    r := SetUpRouter()
    r.PUT("/company/:id", UpdateCompanyHandler)
    company := Company{
        ID: `2`,
        Name: "Demo Company",
        CEO: "Demo CEO",
        Revenue: "35 million",
    }
    jsonValue, _ := json.Marshal(company)
    reqFound, _ := http.NewRequest("PUT", "/company/"+company.ID, bytes.NewBuffer(jsonValue))
    w := httptest.NewRecorder()
    r.ServeHTTP(w, reqFound)
    assert.Equal(t, http.StatusOK, w.Code)

    reqNotFound, _ := http.NewRequest("PUT", "/company/12", bytes.NewBuffer(jsonValue))
    w = httptest.NewRecorder()
    r.ServeHTTP(w, reqNotFound)
    assert.Equal(t, http.StatusNotFound, w.Code)
}

这发送了两个HTTPPUT 请求到company/:id 端点。一个有一个有效载荷和一个有效的公司ID,另一个有一个不存在的ID。有效的调用将返回一个成功的响应代码,而无效的调用则回应为StatusNotFound

更新main_test.go 文件中的导入部分。

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/gin-gonic/gin"
    "github.com/rs/xid"
    "github.com/stretchr/testify/assert"
)

在本地运行测试

现在,通过发出这个命令来运行测试。

go test

Test out non verbose

要禁用Gin调试日志并启用verbose模式,请用-V 标志来运行该命令。

GIN_MODE=release go test -v

Testing output verbose

自动进行测试

通过在CircleCI上创建一个持续集成管道来实现测试的自动化。为了添加所需的配置,创建一个名为.circleci 的文件夹,并在其中创建一个名为config.yml 的新文件。打开新文件,将这段代码粘贴进去。

version: 2.1
orbs:
  go: circleci/go@1.7.1
jobs:
  build:
    executor:
      name: go/default
      tag: "1.16"
    steps:
      - checkout
      - go/load-cache
      - go/mod-download
      - go/save-cache
      - run:
          name: Run tests
          command: go test -v

这个脚本为CircleCI拉来了围棋的球体。这个球体允许执行与Go相关的普通任务,如安装Go、下载模块和缓存。然后,它检查出远程版本库,并发出命令来运行我们的测试。

连接到CircleCI

登录到您的CircleCI账户。如果你用GitHub账户注册,你所有的仓库都会在你的项目仪表板上出现。

Set up project

点击Set Up Project按钮。你会被提示是否已经在你的项目中为CircleCI定义了配置文件。 输入分支名称(在本教程中,我们使用main )。点击 "设置项目"按钮,完成这一过程。

Select config

通过点击工作流程中的一项工作,你可以查看所有的步骤。

Select config

你可以通过点击某项工作获得进一步的细节--例如Run tests 工作。

Pipeline output

这就是你的工作!

结论

在GitHub上有超过5万颗星,令人惊讶的是,Gin正在获得更多的青睐,并逐渐成为Golang开发者中构建高效API的首选。

在本教程中,我向你展示了如何使用Golang和Gin构建一个REST API。我带领大家为每个端点写了一个单元测试,并使用GitHub和CircleCI为其建立了一个持续集成管道。