在用Gin框架编写了一个web server之后,我们如果需要测试handlers接口函数的话,主要可以采用两种方式来进行。
第一种是部署web server,然后通过浏览器或其他http请求模拟工具来手动模拟真实的http请求,发送http请求之后,解析返回的响应,查看响应是否符合预期;这种做法比较麻烦,而且测试结果不太可靠。
第二种是使用httptest结合testing来实现针对handlers接口函数的单元测试。
下面以对四个接口做相应的单元测试为例,分享基于Gin的单元测试的一些方法:
接口名称 |
请求地址 |
请求类型 |
响应数据类型 |
响应数据 |
OnGetStringRequest |
/getString |
get |
string |
success |
OnPracticeRequest |
/practice |
get |
html页面 |
practice.html |
OnLoginRequestForForm |
/loginForm |
post |
json |
|
OnLoginRequestForJson |
/loginJson |
post |
json |
|
一、示例接口代码:
OnGetStringRequest:
// OnGetStringRequest 返回success字符串的接口 func OnGetStringRequest(c *gin.Context) { c.String(http.StatusOK, "success") }
OnPracticeRequest:
// OnPracticeRequest 返回practice.html页面的接口 func OnPracticeRequest(c *gin.Context) { c.HTML(http.StatusOK,"practice.html",gin.H{}) }
OnLoginRequestForForm:
// OnLoginRequestForForm 以表单形式传递参数的登录接口 func OnLoginRequestForForm(c *gin.Context) { req := &User{} if err := c.ShouldBindWith(req, binding.Form); err != nil { log.Printf("err:%v",err) c.JSON(http.StatusOK, gin.H{ "errno":"1", "errmsg":"参数不匹配", "data":"", }) return } c.JSON(http.StatusOK, gin.H{ "errno":"0", "errmsg":"", "data":req, }) }
OnLoginRequestForJson:
// OnLoginRequestForJson 以Json形式传递参数的登录接口 func OnLoginRequestForJson(c *gin.Context) { req := &User{} if err := c.ShouldBindWith(req, binding.JSON); err != nil { log.Printf("err:%v",err) c.JSON(http.StatusOK, gin.H{ "errno":"1", "errmsg":"参数不匹配", "data":"", }) return } c.JSON(http.StatusOK, gin.H{ "errno":"0", "errmsg":"", "data":req, }) }
二、调用的一些结构体和工具函数:
User结构体代码:
//承接前端传过来的json数据或form表单数据 type User struct { Username string `form:"username" json:"username" binding:"required"` Password string `form:"password" json:"password" binding:"required"` Age int `form:"age" json:"age" binding:"required"` }
LoginResponse结构体代码:
// LoginResponse 登录接口的响应参数 type LoginResponse struct { Errno string `json:"errno"` Errmsg string `json:"errmsg"` Data User `json:"data"` }
调用的工具函数:
// ParseToStr 将map中的键值对输出成querystring形式 func ParseToStr(mp map[string]string) string { values := "" for key, val := range mp { values += "&" + key + "=" + val } temp := values[1:] values = "?" + temp return values }
三、单元测试编写步骤:
1. 初始化路由
func init(){
// 初始化路由
router = gin.Default()
router.GET("/getString", OnGetStringRequest)
router.POST("/loginForm", OnLoginRequestForForm)
router.POST("/loginJson", OnLoginRequestForJson)
router.LoadHTMLGlob("E:/mygo/resources/pages/*") //定义模板文件路径
router.GET("/practice", OnPracticeRequest)
}
当接口涉及到对数据库的相关操作时,可以将数据库服务以中间件的形式加到gin的Context中如下图所示:
2. 包装构造http请求的函数(以便测试函数直接调用发起不同种类的http请求)
2.1构造get请求
// Get 根据特定请求uri,发起get请求返回响应 func Get(uri string, router *gin.Engine) []byte { // 构造get请求 req := httptest.NewRequest("GET", uri, nil) // 初始化响应 w := httptest.NewRecorder() // 调用相应的handler接口 router.ServeHTTP(w, req) // 提取响应 result := w.Result() defer result.Body.Close() // 读取响应body body,_ := ioutil.ReadAll(result.Body) return body }
2.2构造post请求,以表单形式传递参数
// PostForm 根据特定请求uri和参数param,以表单形式传递参数,发起post请求返回响应 func PostForm(uri string, param map[string]string, router *gin.Engine) []byte { // 构造post请求,表单数据以querystring的形式加在uri之后 req := httptest.NewRequest("POST", uri+ParseToStr(param), nil) // 初始化响应 w := httptest.NewRecorder() // 调用相应handler接口 router.ServeHTTP(w, req) // 提取响应 result := w.Result() defer result.Body.Close() // 读取响应body body, _ := ioutil.ReadAll(result.Body) return body }
2.3构造post请求,以Json形式传递参数
// PostJson 根据特定请求uri和参数param,以Json形式传递参数,发起post请求返回响应 func PostJson(uri string, param map[string]interface{}, router *gin.Engine) []byte { // 将参数转化为json比特流 jsonByte,_ := json.Marshal(param) // 构造post请求,json数据以请求body的形式传递 req := httptest.NewRequest("POST", uri, bytes.NewReader(jsonByte)) // 初始化响应 w := httptest.NewRecorder() // 调用相应的handler接口 router.ServeHTTP(w, req) // 提取响应 result := w.Result() defer result.Body.Close() // 读取响应body body,_ := ioutil.ReadAll(result.Body) return body }
3. 编写测试函数
3.1针对OnGetStringRequest接口的测试函数
// TestOnGetStringRequest 测试以Get方式获取一段字符串的接口 func TestOnGetStringRequest(t *testing.T) { // 初始化请求地址 uri := "/getString" // 发起Get请求 body := Get(uri, router) fmt.Printf("response:%v\n", string(body)) // 判断响应是否与预期一致 if string(body) != "success" { t.Errorf("响应字符串不符,body:%v\n",string(body)) } }
3.2针对OnPracticeRequest接口的测试函数
// TestOnPracticeRequest 测试以Get方式获取practice.html页面的接口 func TestOnPracticeRequest(t *testing.T) { // 初始化请求地址 uri := "/practice" // 发起Get请求 body := Get(uri, router) fmt.Printf("response:%v\n", string(body)) // 判断响应是否与预期一致 html,_ := ioutil.ReadFile("E:/mygo/resources/pages/practice.html") htmlStr := string(html) if htmlStr != string(body) { t.Errorf("响应数据不符,body:%v\n",string(body)) } }
3.3针对OnLoginRequestForForm接口的测试函数
// TestOnLoginRequestForForm 测试以表单形式传递参数的登录接口 func TestOnLoginRequestForForm(t *testing.T) { // 初始化请求地址和请求参数 uri := "/loginForm" param := make(map[string]string) param["username"] = "valiben" param["password"] = "123" param["age"] = "1" // 发起post请求,以表单形式传递参数 body := PostForm(uri, param, router) fmt.Printf("response:%v\n", string(body)) // 解析响应,判断响应是否与预期一致 response := &LoginResponse{} if err := json.Unmarshal(body, response); err != nil { t.Errorf("解析响应出错,err:%v\n",err) } if response.Data.Username != "valiben" { t.Errorf("响应数据不符,username:%v\n",response.Data.Username) } }
3.4针对OnLoginRequestForJson接口的测试函数
// TestOnLoginRequestForJson 测试以Json形式传递参数的登录接口 func TestOnLoginRequestForJson(t *testing.T) { // 初始化请求地址和请求参数 uri := "/loginJson" param := make(map[string]interface{}) param["username"] = "valiben" param["password"] = "123" param["age"] = 1 // 发起post请求,以Json形式传递参数 body := PostJson(uri, param, router) fmt.Printf("response:%v\n", string(body)) // 解析响应,判断响应是否与预期一致 response := &LoginResponse{} if err := json.Unmarshal(body, response); err != nil { t.Errorf("解析响应出错,err:%v\n",err) } if response.Data.Username != "valiben" { t.Errorf("响应数据不符,username:%v\n",response.Data.Username) } }
4. 运行单元测试,查看测试结果
执行 go test ./ 运行测试代码,测试结果如下
四、总结
基于Gin的单元测试的主要步骤就是先要初始化路由,设定handler接口函数拦截的http请求地址(不需要设定监听端口号);然后通过"net/http/httptest"包的NewRequest(method, target string, body io.Reader)方法构造request,第一个参数是请求类型“POST”“GET”之类,第二个参数是请求的URI地址(form表单的参数可以通过querystring的形式附在URI地址后面进行传递),第三个参数是请求的请求body内容(json数据等其他类型的数据可以加在这里进行传递);接着通过NewRecorder()函数构造响应,调用func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request)方法来调用请求处理的接口handlers,返回的响应将写入前面构造的响应中,通过解析响应,查看其中数据即可完成对接口的测试
有疑问加站长微信联系(非本文作者)