读书笔记《building-distributed-applications-in-gin》基础之外的第三节
Technical requirements
要遵循本章中的说明,您将需要以下内容:
- 对上一章的完整理解——本章是上一章的后续,将使用相同的源代码。因此,为了避免重复,一些片段将不再解释。
- 以前使用 Go 测试包的经验。
本章的代码包托管在 GitHub 上,地址为 https: //github.com/PacktPublishing/Building-Distributed-Applications-in-Gin/tree/main/chapter07。
Testing Gin HTTP handlers
到目前为止,我们已经学习了如何使用 Gin 框架设计、构建和扩展分布式 Web 应用程序。在本章中,我们将介绍如何集成不同类型的测试以消除发布时可能出现的错误。我们将从单元测试开始。
笔记
值得一提的是需要采用测试驱动开发(TDD) 在编写可测试代码时提前采取方法。
为了说明如何 为 Gin Web 应用程序编写单元测试,您需要直接进入一个基本示例。让我们以 第 2 章中的 hello world
示例为例, 设置 API 端点。路由器声明和 HTTP 服务器设置已从 main
函数中提取以准备测试,如下面的代码片段所示:
package main import ( "net/http" "github.com/gin-gonic/gin" ) func IndexHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "hello world", }) } func SetupServer() *gin.Engine { r := gin.Default() r.GET("/", IndexHandler) return r } func main() { SetupServer().Run() }
运行 应用程序,然后 在 GET
请求“文字”>本地主机:8080。将返回一个 hello world
消息,如下所示:
curl localhost:8080 {"message":"hello world"}
完成重构后,在Go编程中编写一个单元测试语。为此,请应用以下步骤:
- Define a
main_test.go
file with the following code in the same project directory. TheSetupServer()
method we previously refactored is injected into a test server:package main func TestIndexHandler(t *testing.T) { mockUserResp := `{"message":"hello world"}` ts := httptest.NewServer(SetupServer()) defer ts.Close() resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) if err != nil { t.Fatalf("Expected no error, got %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("Expected status code 200, got %v", resp.StatusCode) } responseData, _ := ioutil.ReadAll(resp.Body) if string(responseData) != mockUserResp { t.Fatalf("Expected hello world message, got %v", responseData) } }
每个测试方法必须以一个
Test
前缀开头——所以,对于<一个 id="_idIndexMarker568">示例,TestXYZ
将是一个有效的测试。前面的代码使用 Gin 引擎设置了一个测试服务器并发出GET
请求。然后,它检查状态代码和响应负载。如果实际结果与预期结果不符,则会抛出错误。因此,测试将失败。 - To run tests in Golang, execute the following command:
go test
虽然您有 使用测试包编写完整测试的能力,但您可以安装第三方包,例如 testify 使用高级断言。为此,请按照下列步骤操作:
- 使用以下命令下载 testify:
Go get github.com/stretchr/testify
- 接下来,更新
TestIndexHandler
以使用 testify 包中的assert
属性对响应的正确性进行一些断言,如下所示:func TestIndexHandler(t *testing.T) { mockUserResp := `{"message":"hello world"}` ts := httptest.NewServer(SetupServer()) 延迟 ts.Close() resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) defer resp.Body.Close() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) responseData, _ := ioutil.ReadAll(resp.Body) assert.Equal(t, mockUserResp, string(responseData)) }
- 执行
go test
命令,您将得到相同的结果。
让我们向前移动 并为前面章节中介绍的 RESTful API 的 HTTP 处理程序编写单元测试。提醒一下,以下架构说明了 REST API 公开的操作:
笔记
API 源代码位于 chapter07
文件夹下的 GitHub 存储库中。建议根据存储库中可用的源代码开始本章。
图像中的 操作在 Gin 默认路由器中注册并分配给不同的 HTTP 处理程序,如下所示:
func main() { router := gin.Default() router.POST("/recipes", NewRecipeHandler) router.GET("/recipes", ListRecipesHandler) router.PUT("/recipes/:id", UpdateRecipeHandler) router.DELETE("/recipes/:id", DeleteRecipeHandler) router.GET("/recipes/:id", GetRecipeHandler) router.Run() }
从 main_test.go
文件开始,并定义一个方法来返回 Gin 路由器的实例。然后,为每个 HTTP 处理程序编写一个测试方法。例如,TestListRecipesHandler
处理程序显示在以下代码片段中:
func SetupRouter() *gin.Engine { router := gin.Default() return router } func TestListRecipesHandler(t *testing.T) { r := SetupRouter() r.GET("/recipes", ListRecipesHandler) req, _ := http.NewRequest("GET", "/recipes", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) var recipes []Recipe json.Unmarshal([]byte(w.Body.String()), &recipes) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, 492, len(recipes)) }
它在 GET /recipes
资源上注册 ListRecipesHandler
处理程序,然后它发出 GET
请求。然后将请求有效负载编码为 recipes
切片。如果 recipes 的数量等于 492
并且状态码是 200-OK
响应,则认为测试成功.否则会抛出错误,测试会失败。
然后,发出 go test
命令,但这一次,禁用 Gin 调试日志并使用 -v
标志启用详细模式, 如下:
GIN_MODE=release go test -v
命令输出如下所示:
笔记
在第 10 章,捕获 Gin 应用程序指标中,我们将介绍如何自定义 Gin 调试日志以及如何将它们发送到集中式日志平台。
同样,为 NewRecipeHandler
处理程序编写一个测试。它只会发布一个新配方并检查返回的响应代码是否为 200-OK
状态。 TestNewRecipeHandler
方法是,如以下代码片段所示:
func TestNewRecipeHandler(t *testing.T) { r := SetupRouter() r.POST("/recipes", NewRecipeHandler) recipe := Recipe{ Name: "New York Pizza", } jsonValue, _ := json.Marshal(recipe) req, _ := http.NewRequest("POST", "/recipes", bytes.NewBuffer(jsonValue)) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) }
在前面的测试方法中,您使用 Recipe
结构声明了一个配方。然后将该结构编组 为 JSON 格式并作为 NewRequest< 的第三个参数添加/代码>函数。
执行测试,TestListRecipesHandler
和 TestNewRecipeHandler
都应该成功,如下:
您现在熟悉为 Gin HTTP 处理程序编写单元测试。继续为其余的 API 端点编写测试。
Generating code coverage reports
在本节中,我们将介绍如何使用 Go 生成覆盖率报告。测试覆盖率描述了通过运行包的测试执行了多少包代码。
运行以下命令以生成一个文件,该文件包含有关您在上一节中编写的测试所覆盖的代码量的统计信息:
GIN_MODE=release go test -v -coverprofile=coverage.out ./...
该命令将运行测试并显示这些测试覆盖的语句的百分比。在以下示例中,我们覆盖了 16.9% 的语句:
生成的 coverage.out
文件包含单元测试覆盖的行数。为简洁起见,已对完整代码进行了裁剪,但您可以在此处查看说明:
mode: set /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:51.41,53.2 1 1 /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:65.39,67.50 2 1 /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:72.2,77.31 4 1 /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:67.50,70.3 2 0 /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:98.42,101.50 3 0 /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:106.2,107.36 2 0
您可以使用超文本标记语言(HTML) 演示,使用 go tool
命令,如下:
go tool cover -html=coverage.out
该命令将在您的默认浏览器上打开 HTML 演示文稿,以绿色显示覆盖的源代码,以红色显示未覆盖的代码,如下面的屏幕截图所示:
现在更容易发现测试涵盖了哪些方法,让我们为 HTTP 处理程序编写一个额外的测试,负责更新现有的配方。为此,请执行以下操作:
- 将以下代码块添加到
main_test.go
文件:func TestUpdateRecipeHandler(t *testing.T) { r := SetupRouter() r.PUT("/recipes/:id", UpdateRecipeHandler) 食谱:=食谱{ ID: "c0283p3d0cvuglq85lpg", 名字:“汤圆”, 成分:[]字符串{ “5个爱达荷州大土豆”, “2个鸡蛋”, “3/4杯磨碎的帕尔马干酪”, “3 1/2 杯通用面粉”, }, } jsonValue, _ := json.Marshal(recipe) reqFound, _ := http.NewRequest("PUT", "/recipes/"+recipe.ID, bytes.NewBuffer(jsonValue)) w := httptest.NewRecorder() r.ServeHTTP(w, reqFound) assert.Equal(t, http.StatusOK, w.Code) reqNotFound, _ := http.NewRequest("PUT", "/recipes/1", bytes.NewBuffer(jsonValue)) w = httptest.NewRecorder() r.ServeHTTP(w, reqNotFound) assert.Equal(t, http.StatusNotFound, w.Code) }
该代码发出 HTTP
PUT
请求。其中包含两个有效的配方 ID 并检查 HTTP 响应代码(
200-OK
)。 - 重新执行测试,覆盖率应该从 16.9% 增加到 39.0%。以下输出证实了这一点:
惊人的!您现在可以运行单元测试并获得代码覆盖率报告。所以,继续前进,测试并覆盖。
虽然单元测试是软件开发的重要组成部分,但同样重要的是,您编写的代码不能单独进行测试。集成和端到端测试通过一起测试应用程序的各个部分,为您提供额外的信心。这些部分可能单独工作得很好,但在大型系统中,代码单元很少单独工作。这就是为什么在下一节中,我们将介绍如何编写和运行集成测试。
Performing integration tests with Docker
集成测试的目的是验证独立开发的组件是否可以协同工作适当地。与单元测试不同,集成测试可以依赖于数据库和外部服务。
到目前为止编写的分布式 Web 应用程序与外部服务 MongoDB 和 Reddit 交互,如以下屏幕截图所示:
- 使用 Docker Compose 运行我们的集成测试所需的服务。以下
docker-compose.yml
文件将启动 MongoDB 和 Redis 容器:version: "3.9" 服务: 雷迪斯: 图片:redis 端口: - 6379:6379 蒙哥达: 图片:mongo:4.4.3 端口: - 27017:27017 环境: - MONGO_INITDB_ROOT_USERNAME=admin - MONGO_INITDB_ROOT_PASSWORD=密码
- 现在,测试 由 RESTful API 公开的每个端点。例如,要测试负责列出所有配方的端点,我们可以使用以下代码块:
func TestListRecipesHandler(t *testing.T) { ts := httptest.NewServer(SetupRouter()) 延迟 ts.Close() resp, err := http.Get(fmt.Sprintf("%s/recipes", ts.URL)) defer resp.Body.Close() assert.Nil(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) 数据,_ := ioutil.ReadAll(resp.Body) var 食谱 []models.Recipe json.Unmarshal(数据, &recipes) assert.Equal(t, len(recipes), 10) }
- 要运行测试,请提供 MongoDB Uniform Resource Identifier (URI) 和数据库 在
go test
命令之前,如 如下:MONGO_URI="mongodb: //admin:password@localhost:27017/test?authSource=admin&readPreference=primary&ssl=false" MONGO_DATABASE=demo REDIS_URI=localhost:6379 去测试
伟大的!测试将成功通过,如下所示:
测试在 /recipes
端点上发出 GET
请求,并验证端点返回的食谱数是否等于 10 .
另一个重要但被忽视的测试是安全测试。必须确保您的应用程序没有重大安全漏洞,否则数据泄露和数据泄露的风险很高。
Discovering security vulnerabilities
有许多 工具可帮助您识别 Gin Web 应用程序中的主要安全漏洞。在本节中,我们将介绍在构建 Gin 应用程序时可以采用的两种工具:Snyk 和 Golang Security Checker (Gosec)。
在接下来的部分中,我们将演示如何使用这些工具来检查 Gin 应用程序中的安全漏洞。
Gosec
Gosec 是一个 用 Golang 编写的工具,用于检查源代码是否存在安全问题 通过扫描 Go 抽象语法树 (AST)。在我们检查 Gin 应用程序代码之前,我们需要安装 Gosec 二进制文件。
可以使用以下 cURL 命令下载二进制文件。这里使用的是 2.7.0 版本:
curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.0
安装命令后,在项目文件夹中运行以下命令。 ./...
参数设置为递归扫描所有 Go 包:
gosec ./...
该命令将识别与未处理错误相关的三个主要问题(Common Weakness Enumeration (CWE) 703 (https://cwe.mitre.org/data/definitions/703.html),如下图所示:
默认情况下,Gosec 将扫描您的项目并根据规则对其进行验证。但是,可以排除某些规则。例如,要排除导致错误未处理问题的规则,请发出以下命令:
gosec -exclude=G104 ./...
笔记
可在此处找到可用规则的完整列表:
https://github.com/securego/gosec#available-rules
命令输出如下所示:
您现在应该能够扫描您的应用程序源代码以查找潜在的安全漏洞或错误来源。
Securing Go modules with Snyk
检测潜在安全漏洞的另一种方法是扫描 Go 模块。 go.mod
文件 包含 Gin Web 应用程序使用的所有依赖项。 Snyk (https://snyk.io) 是一个 软件即服务 (SaaS) 解决方案,用于识别和修复安全性 Go 应用程序中的漏洞。
笔记
Snyk 支持所有主要的编程语言,包括 Java、Python、Node.js、Ruby、Scala 等。
解决方案非常简单。要开始,请执行以下操作:
- 通过使用您的 GitHub 帐户登录来创建一个免费帐户。
- 然后,使用 Node Package Manager< 安装 Snyk 官方 命令行界面 (CLI) /strong> (npm),如下:
npm install -g snyk
- Next, associate your Snyk account with the CLI by running the following command:
snyk auth
上述命令将打开一个浏览器选项卡并重定向您以使用您的 Snyk 帐户对 CLI 进行身份验证。
- Now, you should be ready to scan the project vulnerabilities with the following command:
snyk test
前面的命令将列出所有已识别的漏洞(主要或次要),包括其路径和修复指南,如以下屏幕截图所示:
- 根据输出,Snyk 确定了两个主要问题。其中之一是使用当前版本的 Gin 框架。单击
Info
URL——您将被重定向到一个专用页面,您可以在其中了解有关该漏洞的更多信息,如以下屏幕截图所示: - Most security vulnerabilities can be fixed by upgrading the packages to the latest stable version. Run the following command to upgrade your project dependencies:
go get -u
go.mod
文件中列出的所有依赖项都将升级到最新的可用版本,如以下屏幕截图所示:
对于 发现的漏洞,GitHub 上有一个 open pull 请求,该请求已合并并在 Gin 1.7 版本中可用,如图所示以下屏幕截图:
就是这样——您现在也知道如何使用 Snyk 扫描您的 Go 模块了!
笔记
我们将介绍如何在 持续集成/持续部署 (CI/CD) 管道中嵌入 Snyk "B17115_09_Final_JM_ePub.xhtml#_idTextAnchor146">第 9 章,实施 CI/CD 管道,持续检查应用程序的安全漏洞源代码。
Running Postman collections
在本书中,您学习了如何使用Postman REST 客户端来测试API 端点。除了发送 API 请求外,Postman 还可以通过在集合中定义一组 API 请求来构建测试套件。
要进行此设置,请执行以下操作:
- 打开 Postman 客户端,点击标题栏中的 New 按钮,然后选择 Collection,如下图所示:
- 将弹出一个新窗口 — 将您的集合命名为
Recipes API
并单击 创建 按钮 保存集合。然后,点击 Add request 创建一个新的 API 请求并将其命名为List Recipes
,如下图所示: - 单击 保存 按钮 — 将打开一个带有您给定请求名称的新选项卡。在地址栏中输入
http://localhost:8080/recipes
并选择GET
方法。
好的——现在,一旦 完成,您将在 Tests 部分编写一些 JavaScript 代码。
在 Postman 中,您可以编写将在发送请求(预请求脚本)之前或收到响应之后执行的 JavaScript 代码。让我们在下一节探讨如何实现这一目标。
Scripting in Postman
测试脚本 可用于测试您的 API 是否相应地工作,或检查新功能是否未影响现有请求的任何功能。
- Click on the Tests section and paste the following code:
pm.test("More than 10 recipes", function () { var jsonData = pm.response.json(); pm.expect(jsonData.length).to.least(10) });
该脚本将检查 API 请求返回的配方数量是否等于 10 个配方,如以下屏幕截图所示:
- 按 Send 按钮并检查 Postman 控制台,如以下屏幕截图所示:
您可以在 图 7.19 中看到测试脚本已通过。
您可能已经注意到地址栏中的 API URL 是硬编码的。虽然 这工作正常,但如果您要维护多个环境(沙盒、暂存和生产),则需要一些方法来测试您的 API 端点,而无需重复收集请求.幸运的是,您可以在 Postman 中创建环境变量。
要使用 URL 参数,请执行以下操作:
- 点击右上角的眼睛图标,然后点击编辑。在VARIABLE列中,设置名称和URL,即
http://localhost:8080
,如图以下截图。点击保存: - 返回您的
GET
请求并使用以下 URL 变量。确保从右上角的下拉菜单中选择 Testing 环境,如所示 以下截图: - 现在,继续为 API 请求添加另一个测试脚本。以下脚本将在响应负载中查找特定配方:
pm.test("Gnocchi recipe", function () { var jsonData = pm.response.json(); 发现var = false; jsonData.forEach(recipe => { if (recipe.name == 'Gnocchi') { 发现=真; } }) pm.expect(found).to.true });
- 按 Send 按钮,两个测试脚本都应该成功,如下所示:
让我们更进一步,创建另一个 API 请求,这次是针对负责添加新配方的端点,如以下屏幕截图所示:
为此,请执行以下操作:
- 定义一个测试脚本,检查插入操作成功返回的HTTP状态码是否为
200-OK
码,如下:pm.test("状态码为200 “, 功能 () { pm.response.to.have.status(200); });
- 再定义一个检查inserted的ID是否为24个字符的字符串,如下:
pm.test("Recipe ID is not null", function(){ var id = pm.response.json().id; pm.expect(id).to.be.a("string"); pm.expect(id.length).to.eq(24); })
- Click the Send button. The test script will fail because the actual status code is
401 – Unauthorized
, which is normal because the endpoint expects an authorization header in the HTTP request. You can see the output in the following screenshot:笔记
要了解有关 API 身份验证的更多信息,请返回 第 4 章,构建API 身份验证,获取分步指南。
- 添加具有有效 JSON Web Token (JWT) 的
Authorization
标头。这一次,测试脚本成功通过了! - 您现在在一个集合中有两个不同的 API 请求。通过单击 Run 按钮运行该集合。将弹出一个新窗口,如以下屏幕截图所示:
- 单击 Run Recipes API 按钮上的 ,两个 API 请求都将按顺序执行,如下图所示:
导出 Postman 集合 后,您可以使用 从终端运行它纽曼 (https://github.com/postmanlabs/newman )。
在下一节中,我们将使用 Newman CLI 运行之前的 Postman 集合。
Running collections with Newman
定义了所有 测试后,让我们使用Newman 命令行来执行它们。值得一提的是,您可以更进一步,在您的 CI/CD 工作流程中运行这些测试作为集成后测试,以确保新的 API 更改并且这些功能不会产生任何回归。
要开始,请执行以下操作:
- 使用 npm 安装 Newman。在这里,我们使用的是 5.2.2 版本:
npm install -g newman
- Once installed, run Newman with the exported collection file as an argument, as follows:
newman run postman.json
API 请求应该失败,因为没有定义 URL 参数,如以下屏幕截图所示:
- You can set its value using a
--env-var
flag, as follows:newman run postman.json --env-var "url=http://localhost:8080"
如果所有调用都通过,这应该是输出:
您现在应该能够使用 Postman 自动化您的 API 端点测试 。
笔记
在第 10 章,捕获 Gin 应用程序指标中,我们将介绍如何在成功发布应用程序后在 CI/CD 管道中触发 newman
run
命令。
Further reading
Go 设计模式 作者:Mario Castro Contreras,Packt Publishing