用 Go 开发终端接口服务--测试已写好的接口
项目的测试工作相当重要,它直接影响到项目的质量。通常一个项目,虽然外围测试团队会进行高强度、多维度的测试,但作为开发者的我们,在测试团队正式测试之前,自己编写一些测试案例,输入一些常规的数据,测试接口是否返回正确的数据,这种工作是非常有必要的,开发者不能没有经过测试,就直接扔给测试团队。
编写测试案例,总体思路是:先分别定义一个请求结构体和响应结构体,再定义一个发送 POST 请求的函数,函数接收请求结构体实例,返回响应结构体实例,最后通过 html 页面罗列所有的接口链接,测试者点击链接,链接就调用发送 POST 请求的函数,把函数返回的请求数据和响应数据直接显示出来,验证测试结果是否正确。
我们新建一个 chapter01/src/test 测试包,定义结构体和辅助函数:
这些都是程序化的东西,相对固定,有迹可循,也是必不可少的,然而案例测试数据才是最关键的,它是变化的,需要根据不同的接口,初始化请求结构体实例,放入字典里,给辅助函数调用。
*代码清单 - 案例测试数据片段*
案例测试数据其实就是一个大字典,是键值对结构的,key 是接口的名称标识,value 是 TestCaseReq 实例,我们大部分的精力都放在 TestCaseReq 实例数据上了。
最后我通过 HTML 方式,把大字典的 key 和 value 循环绑定到链接上 ,相当于罗列了所有测试接口的请求链接,给用户点击链接,就把 value (TestCaseReq 实例)传递给辅助函数 PostTestCaseDataToServer,得到一个 响应结构体的实例(TestCaseResp 实例),再把实例的所有字段的数据显示到另外的 html 结果页面上。渲染 HTML 由 controller 控制层来完成:
*代码清单 - 测试控制器代码片段*
*代码清单 - run_api_test_index.html 循环罗列所有的接口链接代码*
本章节核心内容是案例测试数据的构建,其他实体结构,公共函数,HTML 这些代码相对非常固定,几乎不需要变动,所以我们要把主要精力放在数据构建上,多方面模拟不同的情况来测试接口。
编写测试案例,总体思路是:先分别定义一个请求结构体和响应结构体,再定义一个发送 POST 请求的函数,函数接收请求结构体实例,返回响应结构体实例,最后通过 html 页面罗列所有的接口链接,测试者点击链接,链接就调用发送 POST 请求的函数,把函数返回的请求数据和响应数据直接显示出来,验证测试结果是否正确。
我们新建一个 chapter01/src/test 测试包,定义结构体和辅助函数:
*代码清单 - 测试实体结构体代码片段*
package test import "time" // TestCaseReq 测试案例请求结构体 type TestCaseReq struct { Name string URL string Params map[string]interface{} } // TestCaseResp 测试案例请响应结构体 type TestCaseResp struct { Name string URL string ReqJSON string RespJSON string TimeMs time.Duration }请求结构体,包括一个测试的接口名、URL 测试地址、传递给接口的参数(字典方式);响应结构体同样是接口名、URL 测试地址、请求参数 JSON 字符串(是字段转成了 JSON 字符串)、响应参数 JSON 字符串、耗费时长。请求结构体都是 POST 请求接口所需要的东西,响应结构体是我们想在 html 页面看到的东西。
*代码清单 - 测试公共函数代码片段*
package test import ( "bytes" "encoding/json" "io/ioutil" "net/http" "time" "chapter01/src/common" ) // PostTestCaseDataToServer 发送 POST 测试案例请求的函数 func PostTestCaseDataToServer(testCaseReq TestCaseReq) *TestCaseResp { start := time.Now() bytParams, err := json.Marshal(testCaseReq.Params) if err != nil { common.ShowErr(err) return nil } body := bytes.NewBuffer(bytParams) req, err := http.NewRequest("POST", testCaseReq.URL, body) if err != nil { common.ShowErr(err) return nil } req.Header.Set("Content-Type", applicationJSONCharSet) client := &http.Client{} resp, err := client.Do(req) if err != nil { common.ShowErr(err) return nil } defer resp.Body.Close() respJSON, _ := ioutil.ReadAll(resp.Body) testCaseResp := TestCaseResp{ Name: testCaseReq.Name, URL: testCaseReq.URL, ReqJSON: string(bytParams), RespJSON: string(respJSON), TimeMs: time.Since(start), } return &testCaseResp }辅助函数实际上就是发送 POST 请求函数,把请求 JSON 字符串 POST 给我们的服务,服务接口再返回响应 JSON 数据。
这些都是程序化的东西,相对固定,有迹可循,也是必不可少的,然而案例测试数据才是最关键的,它是变化的,需要根据不同的接口,初始化请求结构体实例,放入字典里,给辅助函数调用。
*代码清单 - 案例测试数据片段*
package test import ( "chapter01/src/model" "chapter01/src/util" ) const ( testDomain = "http://localhost:3000" applicationJSONCharSet = "application/json;charset=utf-8" ) // ALLTestCaseReqData 全部测试案例请求数据 func ALLTestCaseReqData() map[string]TestCaseReq { allTestCaseReq := map[string]TestCaseReq{} allTestCaseReq["pathProductList"] = TestCaseReq{ Name: "产品列表", URL: testDomain + "/api/v1/product/list", Params: map[string]interface{}{ "category": 1, "start": 1, "end": 10, }, } allTestCaseReq["pathProductDetail"] = TestCaseReq{ Name: "产品详情页", URL: testDomain + "/api/v1/product/detail", Params: map[string]interface{}{ "productID": 1, }, } allTestCaseReq["pathProductSearch"] = TestCaseReq{ Name: "产品搜索", URL: testDomain + "/api/v1/product/search", Params: map[string]interface{}{ "category": 1, "key": "语言", "start": 1, "end": 10, }, } allTestCaseReq["pathProductAdd"] = TestCaseReq{ Name: "新增一个产品", URL: testDomain + "/api/v1/product/add", Params: map[string]interface{}{ "category": 1, "name": "Go", "intro": "Go 语言的圣经书籍,年度畅销书籍", "price": 56.98, "photoEdit": []model.PhotoArgs{model.PhotoArgs{ Path: "/product/154192547123856140062_b.png", Seq: 1, }, model.PhotoArgs{ Path: "/product/154192547123856140062_b.png", Seq: 2, }, model.PhotoArgs{ Path: "/product/154192547123856140062_b.png", Seq: 3}}, }, } allTestCaseReq["pathProductModify"] = TestCaseReq{ Name: "修改一个产品", URL: testDomain + "/api/v1/product/modify", Params: map[string]interface{}{ "productID": 3, "category": 1, "name": "Go", "intro": "Go 语言的圣经书籍,年度畅销书籍666", "price": 56.98, "photoEdit": []model.PhotoArgs{model.PhotoArgs{ ID: 1, Path: "/product/154192547123856140062_b.png", Seq: 1, }, model.PhotoArgs{ ID: 2, Path: "/product/154192547123856140062_b.png", Seq: 2, }, model.PhotoArgs{ ID: 3, Path: "/product/154192547123856140062_b.png", Seq: 3}}, }, } allTestCaseReq["pathProductDelete"] = TestCaseReq{ Name: "删除一个产品", URL: testDomain + "/api/v1/product/delete", Params: map[string]interface{}{ "productID": 3, }, } allTestCaseReq["pathProductPhotoUpload"] = TestCaseReq{ Name: "产品图片上传", URL: testDomain + "/api/v1/product/photo/upload", Params: map[string]interface{}{ "file": util.PathToBytes("D:\\t.png"), "fileExt": "png", "seq": 1, }, } return allTestCaseReq }案例测试数据随着接口的增多,也会相应增加,做法上没有技术性的难点,我们只需要熟悉业务性的东西,自然就可以按需制造数据,用于发送 POST 请求进行测试。
案例测试数据其实就是一个大字典,是键值对结构的,key 是接口的名称标识,value 是 TestCaseReq 实例,我们大部分的精力都放在 TestCaseReq 实例数据上了。
最后我通过 HTML 方式,把大字典的 key 和 value 循环绑定到链接上 ,相当于罗列了所有测试接口的请求链接,给用户点击链接,就把 value (TestCaseReq 实例)传递给辅助函数 PostTestCaseDataToServer,得到一个 响应结构体的实例(TestCaseResp 实例),再把实例的所有字段的数据显示到另外的 html 结果页面上。渲染 HTML 由 controller 控制层来完成:
*代码清单 - 测试控制器代码片段*
package controller import ( "net/http" "chapter01/src/test" ) // RunTestIndex 循环罗列所有的接口链接 func RunTestIndex(w http.ResponseWriter, req *http.Request) { ctx := map[string]interface{}{} ctx["allTestCaseReq"] = test.ALLTestCaseReqData() r.HTML(w, http.StatusOK, "run_api_test_index", ctx) return } // RunTestResult 显示 POST 请求后的结果页面 func RunTestResult(w http.ResponseWriter, req *http.Request) { key := req.FormValue("key") testCaseReqData := test.ALLTestCaseReqData()[key] testCaseResp := test.PostTestCaseDataToServer(testCaseReqData) ctx := map[string]interface{}{} ctx["testCaseResp"] = testCaseResp r.HTML(w, http.StatusOK, "run_api_test_result", ctx) return }文件夹 template 中的两个 HTML 页面,一个是循环罗列所有的接口链接(run_api_test_index.html),一个是显示 POST 请求后返回的显示结果页面(run_api_test_result.html)
*代码清单 - run_api_test_index.html 循环罗列所有的接口链接代码*
<!DOCTYPE html> <html > <head> <meta charset="UTF-8"> <title>接口测试首页</title> </head> <body> <h3>接口测试链接</h3> <ol> {{range $key,$val:= .allTestCaseReq}} <li><a target="_blank" href="/test/result?key={{$key}}">{{$val.Name}} {{$val.URL}}</a></li> {{end}} </ol> </body> </html>
代码清单 - run_api_test_result.html 显示结果页面代码
<!DOCTYPE html> <html> <head> <title>{{.testCaseResp.Name}} - 测试接口</title> <style type="text/css"> body,table{font-size:12px}h1{font-size:1.4em;color:#2d2d2d}table{table-layout:fixed;empty-cells:show;border-collapse:collapse;width:100%}.table{color:#444;border-top:1px solid #ccc}.table th{background-repeat:repeat-x}.table td,.table th{border-bottom:1px solid #ccc;padding:0.8em 0.6em;word-break:break-all}.table tr.alter{background-color:#eee}.table tr .title{width:130px;color:#555}.table tr .cont{color:#000}</style> </head> <body> <h1>请求的接口:{{.testCaseResp.URL}}</h1> <table class="table"> <tr class="alter"> <td class="title">请求参数:</td> <td class="cont">{{.testCaseResp.ReqJSON}}</td> </tr> <tr> <td class="title">请求类型:</td> <td class="cont">POST</td> </tr> <tr class="alter"> <td class="title">响应内容:</td> <td class="cont">{{.testCaseResp.RespJSON}}</td> </tr> <tr> <td class="title">总计耗时:</td> <td class="cont">{{.testCaseResp.TimeMs}}</td> </tr> </table> </body> </html>
本章节核心内容是案例测试数据的构建,其他实体结构,公共函数,HTML 这些代码相对非常固定,几乎不需要变动,所以我们要把主要精力放在数据构建上,多方面模拟不同的情况来测试接口。
《用 Go 开发终端接口服务》 目录
- 小册介绍
- 前言
- 环境搭建与开发工具选择
- Go 语言基本语法
- Go 语言编码规范
- 快速编写一个 Web 服务器
- 项目整体结构介绍
- 准备项目所需的 Go 类包
- 公共类关键函数
- 定义 model 实体层结构体
- 灵活写 dao 数据层函数
- 按需写 service 服务层逻辑
- 暴露 controller 控制层接口
- 测试已写好的接口
- 把项目部署到服务器
- 保证高性能项目的法宝
- 写在后面
1 楼: 潘军杰 (博主) 发表于 2021-08-20 11:27:55 回复 TA