Go语言的单元测试

Go语言的单元测试

单元测试的概念

测试是专业主义的保障。请注意,测试不是程序员的额外工作,程序员写单元测试是为了确定系统真的可用,只有交付了可用的系统,程序员才算完成,业务方才能满足。所以,不要把它看作额外的工作,而应当看成节省时间和金钱的方法。单元测试的好处,一是可以减少返工时间,二是让开发人员可以更有底气的交付系统。

单元测试的基本要求:

  • 可以快速运行测试
  • 可以在不同环境下测试
  • 可以并行测试

单元测试还是TDD的基础,TDD的概念不在此处赘述,如果感兴趣可选读这篇文章:juejin.cn/post/693617…

Go提供了强大的单元测试库testing,需要时也可以结合Gomock使用。

Package testing

go test

在Go中所有以 _test.go为后缀名的源代码文件都属于测试文件。这部分文件在运行时会被Go忽略,并且可以使用go test命令执行测试文件中的所有测试用例。

go test携带-v参数表示输出完整的测试结果。

go test携带 -cover参数可以查看测试覆盖率。

go test携带 -coverprofile=c.out参数可以将相关信息输出到当前文件夹下面的c.out文件中。

如果只需要测试某几个函数,可以携带 -run参数,它对应一个正则表达式,例如运行go test -run=Set命令就只会执行函数名中带Set的测试函数。

测试函数必须以Test开头,例如:

func TestName(t *testing.T) {
  // todo
}
复制代码

一个简单的单元测试示例

产品代码示例:

package utils

func StrListToString(strList []string) (str string) {
    if len(strList) > 0 {
        for k, v := range strList {
            if k == 0 {
                str = v
            } else {
                str = str + "," + v
            }
        }
        return
    }
    return ""
}
复制代码

测试代码示例:

package utils

import (
    "reflect"
    "testing"
)

type testStrListToStringCase struct {
    strList []string
    want    string
}

var testStrListToStringGroup = []testStrListToStringCase{
    // 测试用例1:单个英文
    {
        strList: []string{"str1"},
        want:    "str1",
    },
    // 测试用例2:多个英文
    {
        strList: []string{"str1", "str2", "str3"},
        want:    "str1,str2,str3",
    },
    // 测试用例3:逗号
    {
        strList: []string{"a,b,c"},
        want:    "a,b,c",
    },
    // 测试用例4:汉字
    {
        strList: []string{"一二三四五"},
        want:    "一二三四五",
    },
    // 测试用例5:空字符串
    {
        strList: []string{""},
        want:    "",
    },
}

func TestStrListToString(t *testing.T) {
    for _, test := range testStrListToStringGroup {
        got := StrListToString(test.strList)
        if !reflect.DeepEqual(got, test.want) {
            t.Errorf("The values of %v is not %v\n", got, test.want)
        }
    }
}
复制代码

我们定义了测试函数TestStrListToString和一个结构体数组testStrListToStringGroup,结构体数组testStrListToStringGroup包含了我们需要的所有测试用例,结构体中的strList字段作为输入的参数,want字段作为我们想要的结果。

在测试函数中循环输入测试用例,执行我们的产品代码,得到的结果可以用反射包的DeepEqual方法来判等,如果测试函数返回的结果和我们想要的不一致,就打印错误提示。

使用IDE自动生成测试代码

一些IDE支持自动生成测试代码的框架,比如可以使用goland编辑器的快捷键command+shift+t 自动生成测试代码(函数、文件、包)。

子测试

在测试函数中可以运行多组测试,使用t.Run(测试用例, 测试函数) 方法即可。

Go官方标准库中的fmt包有如下测试:

var flagtests = []struct {
  in  string
  out string
}{
  {"%a", "[%a]"},
  {"%-a", "[%-a]"},
  {"%+a", "[%+a]"},
  {"%#a", "[%#a]"},
  {"% a", "[% a]"},
  {"%0a", "[%0a]"},
  {"%1.2a", "[%1.2a]"},
  {"%-1.2a", "[%-1.2a]"},
  {"%+1.2a", "[%+1.2a]"},
  {"%-+1.2a", "[%+-1.2a]"},
  {"%-+1.2abc", "[%+-1.2a]bc"},
  {"%-1.2abc", "[%-1.2a]bc"},
}

func TestFlagParser(t *testing.T) {
  var flagprinter flagPrinter
  for _, tt := range flagtests {
    t.Run(tt.in, func(t *testing.T) {
      s := Sprintf(tt.in, &flagprinter)
      if s != tt.out {
        t.Errorf("got %q, want %q", s, tt.out)
      }
    })
  }
}
复制代码

上例定义了测试用例组flagtests,在测试函数TestFlagParser中使用了子测试方法t.Run。

Package httptest

网络测试

除了testing包,Go还提供了专门用于测试HTTP请求的httptest包。

请看以下示例:

func TestHandler(t *testing.T) {

  // todo

  r := SetupRouter()

  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      req := httptest.NewRequest(
        "POST",                  
        "/test",  
      )

      w := httptest.NewRecorder()

      r.ServeHTTP(w, req)

      assert.Equal(t, http.StatusOK, w.Code)

      var resp map[string]string
      err := json.Unmarshal([]byte(w.Body.String()), &resp)
      assert.Nil(t, err)
      assert.Equal(t, tt.expect, resp["msg"])
    })
  }
}
复制代码

我们先用SetupRouter方法创建了一个路由对象,使用不同的web框架会用到不同的方法,只要保证此处能拿到Router对象就行。

在子测试t.Run中,我们用httptest.NewRequest方法mock了一个http请求,再用httptest.NewRecorder方法mock了一个响应记录器。

接着执行路由对象下的ServeHTTP方法调用接口,解析响应体w.Body即得到接口返回的内容。

在代码外调用api测试

上例使用了httptest包在代码内部调用请求进行测试,如果想要在外部直接调用api进行测试,可以使用接口文档工具Yapi或API管理工具Postman等。也可以用Go代码写一下GET和POST的网络请求直接调用api。

分类:
后端
标签: