gtest模块提供了简便化的、轻量级的、常用的单元测试方法。是基于标准库testing的功能扩展封装,主要增加实现了以下特性:

  • 单元测试用例多测试项的隔离。
  • 增加常用的一系列测试断言方法。
  • 断言方法支持多种常见格式断言。提高易用性。
  • 测试失败时的错误信息格式统一。

gtest设计为比较简便易用,可以满足绝大部分的单元测试场景,如果涉及更复杂的测试场景,可以考虑第三方的testifygoconvey等测试框架。

使用方式

import "github.com/gogf/gf/v2/test/gtest"

接口文档

https://pkg.go.dev/github.com/gogf/gf/v2/test/gtest

func C(t *testing.T, f func(t *T))
func Assert(value, expect interface{})
func AssertEQ(value, expect interface{})
func AssertGE(value, expect interface{})
func AssertGT(value, expect interface{})
func AssertIN(value, expect interface{})
func AssertLE(value, expect interface{})
func AssertLT(value, expect interface{})
func AssertNE(value, expect interface{})
func AssertNI(value, expect interface{})
func Error(message ...interface{})
func Fatal(message ...interface{})

简要说明

  1. 使用C方法创建一个Case,表示一个单元测试用例。一个单元测试方法可以包含多个C,每一个C包含的用例往往表示该方法的其中一种可能性测试。
  2. 断言方法Assert支持任意类型的变量比较。AssertEQ进行断言比较时,会同时比较类型,即严格断言。
  3. 使用大小比较断言方法如AssertGE时,参数支持字符串及数字比较,其中字符串比较为大小写敏感。
  4. 包含断言方法AssertINAssertNI支持slice类型参数,暂不支持map类型参数。

用于单元测试的包名既可以使用包名_test,也可直接使用包名(即与测试包同名)。两种使用方式都比较常见,且在Go官方标准库中也均有涉及。但需要注意的是,当需要测试包的私有方法/私有变量时,必须使用包名命名形式。且在使用包名命名方式时,注意仅用于单元测试的相关方法(非Test*测试方法)一般定义为私有,不要公开。

使用示例

例如gstr模块其中一个单元测试用例:

package gstr_test

import (
    "github.com/gogf/gf/v2/test/gtest"
    "github.com/gogf/gf/v2/text/gstr"
    "testing"
)

func Test_Trim(t *testing.T) {
    gtest.C(t, func(t *gtest.T) {
        t.Assert(gstr.Trim(" 123456\n "),      "123456")
        t.Assert(gstr.Trim("#123456#;", "#;"), "123456")
    })
}

也可以这样使用:

package gstr_test

import (
    . "github.com/gogf/gf/v2/test/gtest"
    "github.com/gogf/gf/v2/text/gstr"
    "testing"
)

func Test_Trim(t *testing.T) {
    C(t, func() {
        Assert(gstr.Trim(" 123456\n "),      "123456")
        Assert(gstr.Trim("#123456#;", "#;"), "123456")
    })
}

一个单元测试用例可以包含多个C,一个C也可以执行多个断言。 断言成功时直接PASS,但是如果断言失败,会输出如下类似的错误信息,并终止当前单元测试用例的继续执行(不会终止后续的其他单元测试用例)。

=== RUN   Test_Trim
[ASSERT] EXPECT 123456#; == 123456
1. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/text/gstr/gstr_z_unit_trim_test.go:20
2. /Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/text/gstr/gstr_z_unit_trim_test.go:18
--- FAIL: Test_Trim (0.00s)
FAIL
  • No labels

7 Comments

  1. 有一个小疑问, 请问该如何测试一个接口呢? 类似于 fiberapp.Test 的功能, goframe 其他部分我都很喜欢, 就这个测试的地方不是弄的很明白.粘贴一个 fiber 的测试: (🚀 App - Fiber (gofiber.io))

    请问有类似的方法在 gf 当中做到吗? 我尝试使用 server.ServeHttp 接口来做测试, 但是得到了空指针的错误.

    // Create route with GET method for test:
    app.Get("/", func(c *fiber.Ctx) error {
      fmt.Println(c.BaseURL())              // => http://google.com
      fmt.Println(c.Get("X-Custom-Header")) // => hi
    
      return c.SendString("hello, World!")
    })
    
    // http.Request
    req := httptest.NewRequest("GET", "http://google.com", nil)
    req.Header.Set("X-Custom-Header", "hi")
    
    // http.Response
    resp, _ := app.Test(req)
    
    // Do something with results:
    if resp.StatusCode == 200 {
      body, _ := ioutil.ReadAll(resp.Body)
      fmt.Println(string(body)) // => Hello, World!
    }
    1. 你好,goframe的单侧可以参考ghttp包中的_test.go文件,例如:https://github.com/gogf/gf/blob/2970864158bd5ae694659f5bed46d11275803b38/net/ghttp/ghttp_unit_request_test.go#L590。坦白地讲,我没看懂fiber提供一个Test方法的作用。在业务上一个路由的单侧不仅是测试一个路由这么简单,其实还会涉及到项目的一些预初始化、基本配置,甚至包括ORM Mock,所以我觉得可能本地启动Server,并通过gtest.C创建一个测试用例模拟业务场景、完整地请求业务接口会可靠谱一些。

      1. 感谢回复. 我也赞同您的观点. 但是如果每次测试都需要整个应用链路都初始化一遍的话, 可能耗费的时间会比较多. TDD 做起来就比较麻烦. 我想寻求一种更简便的方式, 在 django 中有个 TestClient, 可以通过对这个 client 直接发起对开发接口调用, 而不需要启动服务器. 我是做上游接口的, 对下游提供接口服务, 但是我又有好几个个上游提供方. 在接口二次开发的过程中, 就经常需要测试某个单一接口的工作是否正常, 所以测试频率就很高. 之前在用 GF 的时候, 一直都是用 PostMan 做测试请求, 但是只能得到输出后的数据, 如果出错了, 还需要在服务端和 PostMan 两边来回切.  但是在大多数的场景下, 针对单一的接口, 可能就只需要一个可以运行的 "最小系统" 就行了, 所以我觉得这就是 fiber.Test 的意义吧, 这个接口提供了一个可以运行的最小环境 , 所以就想来请教一下 GF 是否有差不多的方法.

        不过看了 ghttp 的测试方法, 我觉得这比我 PostMan 要好太多了. 我可以自己写一个 Helper 来测试了.非常感谢.

      2. 是不是意味着,某些不涉及到其他库的func(比如一个加减法函数),无论是在service,或者controller层,可以使用单测。

        但是如果涉及到如数据库等,需要初始化的,就没办法做单测了?

        1. 也可以,但是需要设计好DAO层,通过接口解耦,比如:


          func GetUserInfo(id int){
          	info := dao.Get(id)
          }
          
          // dao
          func Get(id int){
          	// 去数据库查
          }

          要改成:

          func GetUserInfo(id int){
          	info := dao.UserImpl.Get(id)
          }
          
          // dao
          type UserInterface interface{
          	Get(id int)
          }
          
          type userImpl struct{
          }
          
          func (u *userImpl)Get(id int){
          	// 去数据库查
          }

          这样做单测时,就能mock一个UserInterface

          1. 你这个例子我不是很明白 按你这样写出来是有问题的 能帮忙看看么


  2. 1) 在用gin开发的时候,我会用server.ServerHttp这个方法去测试某个路由,正如 ucag 说的,在goframe中使用这个方法会有空指针的问题,这是因为ghttp.Server这个结构体的的很多字段的初始化在Start()方法中,比如:session 和cookie.  那么是不是可以单独使用一个server的初始化方法?这样使得我们得到一个可以测试的server?

    2) 在测试中间件时(也即是测试:ghttp.HandlerFunc),需要自己构建一个ghttp.Request, ghttp.Response对象,但是http.newRequest, http.newResponse都是包外不可见的。如果自己要构建一个Request, Response结构体,但是Response.ResponseWriter的所有字段都是不可见的。这使得我们自己构建一个ghttp.Request, ghttp.Response来用作测试变得不可能了。

    郭强 那么我们该怎么测试中间件尼?

    我看了ghttp_test,那里似乎是启动一个server, 然后使用http客户端发送请求给测试用例启动的server。这种测试方法,我想是没法使用goland/vscode的debug功能了。