• 在Go中如何做单元测试?
  • 发布于 2个月前
  • 247 热度
    0 评论
单元测试有什么用?

单元测试是检测你写的一个函数是否具备安全性的一次检测。比如,当你写了一个函数,传入你期望的值,该函数是能正常执行的。但是当你传入一个非合理的值后,你写的函数直接宕机,而不是返回err。那就说明你写的函数存在逻辑漏洞。一个通过单元测试的函数,无论传入什么类型的值,返回的要么是结果,要么是error,绝对不会停止运行。


go test工具
在golang语言中存在测试依赖go test命令,go test命令会对包路径下的所有_test.go为后缀的源代码文件进行测试,不会被go build编译到可执行文件中。
在*_test.go文件中,包含三种类型的函数:单元测试函数、基准测试函数、示例函数

go test 命令会对遍历*_test.go文件中的符合上述规范的函数,生成一个临时的main包用于调用相应的测试函数,重构、运行、报告、测试,最后清除测试中的生成的临时文件。


测试函数
每个测试函数都必须导入testing包。
// 用于对str字符串的子串截取
func subString(str string, start, end int) string {
   //将str转换为[]rune,支持Unicode编码
   rs := []rune(str)
   length := len(rs)
   if start < 0 || start > length {
      panic("start is wrong")
   }
   if end < start || end > length {
      panic("end is wrong")
   }
   return string(rs[start:end])
}
上述函数传入str、start、end参数,分别表示原始字符串,起始下标,结束下标。目的是为了获取str的子串。这里有个知识点,是关于go语言的编码的:
在golang中,其采纳的编码方式是UTF8的支持中文编码方式,而非Unicode。在UTF8中,一个汉字是用三个字节来表示的。举个列子,str := "hang行",其len(str)==7,如果你从start ==5 ,end == 6切割就会出现乱码,相当于你截取了汉字的三分之一。当str转换为[]rune类型后,len([]rune(str)) == 5,这个时候汉字就是一个整体,就能完整的截取"行"这个汉字。
func TestCode(t *testing.T) {
   // []rune与Unicode
   str := "i am your 爸爸"
   fmt.Printf("str的原始长度:%d\n", len(str))
   fmt.Printf("str的[]rune类型长度:%d\n", len([]rune(str)))
}
打印信息:
API server listening at: 127.0.0.1:64049
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN   TestCode
str的原始长度:16
str的[]rune类型长度:12
--- PASS: TestCode (0.00s)
PASS
对[]rune的测试完毕,回到测试函数中:
func TestSubString(t *testing.T) {
   got := subString("hang", 0, 1)
   want := "h"
   if !reflect.DeepEqual(got, want) {
      t.Error("subString test is failed")
   }
}
打印信息:
API server listening at: 127.0.0.1:57824
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN   TestSubString
--- PASS: TestSubString (0.00s)
PASS
在该测试函数中,got为函数返回信息;want为期望值。经过reflect.DeepEqual(got,want)函数进行匹配比较。
reflect.DeepEqual(got,want)函数:用于比较两个值的深度相等性。它可以用来在测试中判断期望的值和实际得到的值是否相等。reflect.DeepEqual对比的是值的具体内容,而不仅仅是内存地址或类型。用于确保值的类型和结构是一致的。

目前常用的testing.T类型的打印信息函数是上诉这几个。如果对其他函数感兴趣,可以查阅资料。测试函数是能够执行了,但是只能针对一个测试用例。如果有多个测试用例呢?这就引入了子测试函数的使用:
func TestSubString(t *testing.T) {

   type test struct {
      str   string
      start int
      end   int
      want  interface{}
   }
   es := map[string]test{
      "basic":   {str: "basic", start: 0, end: 4, want: "basi"}, //basi
      "basicCN": {str: "你好world", start: 0, end: 1, want: "你"},  //"你"
   }
   for name, testValue := range es {
      t.Run(name, func(t *testing.T) {
         got := subString(testValue.str, testValue.start, testValue.end)
         if !reflect.DeepEqual(got, testValue.want) {
            t.Errorf("the name== %s got %v, want %v", t.Name(), got, testValue.want)
         }
      })
   }
}
// 堆代码 duidaima.com
API server listening at: 127.0.0.1:64621
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN   TestSubString
=== RUN   TestSubString/basic
=== RUN   TestSubString/basicCN
--- PASS: TestSubString (0.00s)
    --- PASS: TestSubString/basic (0.00s)
    --- PASS: TestSubString/basicCN (0.00s)
PASS

说明:在我写的函数SubString中,有 panic("start is wrong") 语句,这个结果在测试单元中是无法want的,即这种结果是无法测试。但是这个函数对我的项目是很重要的,未达到start和end就必须要panic。无需去深究,你可以将panic换成error.new("XXXX")。测试函数的部分知识就讲解完毕了,文章后续有案例实战。


基准测试
基准测试是在一定的工作负载下检测程序性能的一种方法。函数一定是先通过测试函数之后,才开始对该函数性能的检测。很多程序员在开发了一套函数时,就会有一种心态:能跑就行。其实这样想也没问题,工作嘛。基准测试也是检测自身编写代码的性能。很简单一个道理,同样是写一样功能的代码,为什么就用其他的,而不用你的,因为其他的在性能测试上超越了你。
基准测试是以Benchmark为开头的,需要以*testing.B类型的参数。
func BenchmarkSubstring(b *testing.B) {
   for i := 0; i < b.N; i++ {
      subString("Benchmark", 0, 5)
   }
}

goos: windows
goarch: amd64
pkg: sspaas.io/gpu-manage/utils/data
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkSubstring
BenchmarkSubstring-8    14777178                81.77 ns/op
PASS
相较于单元测试,基准测试使用得不是很多。
BenchmarkSubstring-8    14777178                81.77 ns/op

表示Substring函数在迭代14777178次下,平均每次消耗81.77 纳秒。b.N默认是很大的一个数,你也可以自定义迭代次数。一般是不要去动b.N基准测试还有很多知识点,包括内存消耗的检测、测试代码的覆盖率等。一般来说,作为开发人员仅仅测试一下迭代时间即可。若想了解更多,自行查阅资料。


示例函数
在单元测试中,示例函数(Example Functions)是一种特殊的函数,用于提供对被测试函数的示例使用方法、参数和期望输出的演示。
func ExampleFunction() {
result := YourFunction(input)
fmt.Println(result)
// Output: expected_output
}
闲的没事的人,才去写示例函数。有这时间,还不如去泡杯茶····
实战
对aes加密解密的aes.go文件进行单元测试
import (
   "bytes"
   "crypto/aes"
   "crypto/cipher"
   "encoding/base64"
)

// AesEncrypt AES加密
func AesEncrypt(orig string, key string) string {
   // 转换成字节数组
   origData := []byte(orig)
   k := []byte(key)
   // 分组秘钥
   block, _ := aes.NewCipher(k)
   // 获取秘钥块的长度
   blockSize := block.BlockSize()
   // 补全码
   origData = PKCS7Padding(origData, blockSize)
   // 加密模式
   blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
   // 创建数组
   cryted := make([]byte, len(origData))
   // 加密
   blockMode.CryptBlocks(cryted, origData)

   return base64.StdEncoding.EncodeToString(cryted)
}

// AesDecrypt AES解密
func AesDecrypt(cryted string, key string) string {
   // 转成字节数组
   crytedByte, _ := base64.StdEncoding.DecodeString(cryted)
   k := []byte(key)

   // 分组秘钥
   block, _ := aes.NewCipher(k)
   // 获取秘钥块的长度
   blockSize := block.BlockSize()
   // 加密模式
   blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
   // 创建数组
   orig := make([]byte, len(crytedByte))
   // 解密
   blockMode.CryptBlocks(orig, crytedByte)
   // 去补全码
   orig = PKCS7UnPadding(orig)
   return string(orig)
}

// PKCS7Padding 补码
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
   padding := blocksize - len(ciphertext)%blocksize
   padtext := bytes.Repeat([]byte{byte(padding)}, padding)
   return append(ciphertext, padtext...)
}

// PKCS7UnPadding 去码
func PKCS7UnPadding(origData []byte) []byte {
   length := len(origData)
   unpadding := int(origData[length-1])
   return origData[:(length - unpadding)]
}

import (
   "reflect"
   "testing"
)
/*----------------------单元测试----------------------------------------*/
const key = "q+#@,6%ej-QP^mbc"

func TestAesEncrypt(t *testing.T) {
   type test struct {
      org  string
      key  string
      want string
   }
   es := map[string]test{
      "passed": {org: "basic", key: key, want: "2+1s9cqk1NzIUnq+G3VgEg=="}, //passed
   }
   for _, value := range es {
      got := AesEncrypt(value.org, value.key)
      if !reflect.DeepEqual(got, value.want) {
         t.Errorf("the name== %s got %v, want %v", t.Name(), got, value.want)
      }
   }
}

/*
API server listening at: 127.0.0.1:54671
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN   TestAesEncrypt
--- PASS: TestAesEncrypt (0.00s)
PASS
*/

func BenchmarkAesEncrypt(b *testing.B) {
   for i := 0; i < b.N; i++ {
      AesEncrypt("test", key)
   }
}
/*pkg: sspaas.io/gpu-manage/utils/aes
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkAesEncrypt
BenchmarkAesEncrypt-8            1000000              1031 ns/op
PASS
*/

func TestAesDecrypt(t *testing.T) {
   type test struct {
      org  string
      key  string
      want string
   }
   es := map[string]test{
      "passed": {org: "2+1s9cqk1NzIUnq+G3VgEg==", key: key, want: "basic"}, //passed
   }
   for _, value := range es {
      got := AesDecrypt(value.org, value.key)
      if !reflect.DeepEqual(got, value.want) {
         t.Errorf("the name== %s got %v, want %v", t.Name(), got, value.want)
      }
   }
}
/*API server listening at: 127.0.0.1:54693
WARNING: undefined behavior - version of Delve is too old for Go version 1.19.0 (maximum supported version 1.18)
=== RUN   TestAesDecrypt
--- PASS: TestAesDecrypt (0.00s)
PASS
*/

func BenchmarkAesDecrypt(b *testing.B) {
   for i := 0; i < b.N; i++ {
      AesDecrypt("2+1s9cqk1NzIUnq+G3VgEg==", key)
   }
}
/*pkg: sspaas.io/gpu-manage/utils/aes
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkAesDecrypt
BenchmarkAesDecrypt-8            1334907               883.0 ns/op
PASS
*/
总结
golang基于单元测试的教程常用的技巧也就这些了。制作不容易,给个赞吧。

用户评论