Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang单元测试

详解Golang中单元测试的使用

作者:想去海南摘椰子

单元测试是检测你写的一个函数是否具备安全性的一次检测,这篇文章主要为大家详细介绍了Golang中单元测试的具体使用,希望对大家有所帮助

介绍

单元测试有什么用?

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

go test工具

在golang语言中存在测试依赖go test命令,go test命令会对包路径下的所有_test.go为后缀的源代码文件进行测试,不会被go build编译到可执行文件中。

在*_test.go文件中,包含三种类型的函数:单元测试函数、基准测试函数、示例函数

类型文件名作用
测试函数TestXxxx测试程序的一些逻辑行为是否正确
基准函数BenchchmarkXxxx测试函数的性能
示例函数ExampleXxxx为文档提供示例文档

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类型的方法作用
Error输出一个错误消息,并标记该测试为失败。
Errorf格式化输出一个错误消息,并标记该测试为失败。
Fail标记该测试为失败。
FailNow标记该测试为失败,并立即停止当前测试函数的执行。
Fatal输出一个致命错误消息,并终止测试执行
Fatalf格式化输出一个致命错误消息,并终止测试执行。
Name返回当前测试的名称。
Run运行一个子测试函数,并指定其名称。

目前常用的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)
         }
      })
   }
}

打印信息:

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中单元测试的使用的文章就介绍到这了,更多相关Golang单元测试内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文