一步步教你编写可测试的Go语言代码

第一个测试 “Hello Test!”

首先,在我们$GOPATH/src目录下创建hello目录,作为本文涉及到的所有示例代码的根目录。

然后,新建名为hello.go的文件,定义一个函数hello() ,功能是返回一个由若干单词拼接成句子:

package hello

func hello() string {
 words := []string{"hello", "func", "in", "package", "hello"}
 wl := len(words)

 sentence := ""
 for key, word := range words {
  sentence += word
  if key < wl-1 {
   sentence += " "
  } else {
   sentence += "."
  }
 }
 return sentence
}

接着,新建名为hello_test.go的文件,填入如下内容:

package hello

import (
 "fmt"
 "testing"
)

func TestHello(t *testing.T) {
 got := hello()
 expect := "hello func in package hello."

 if got != expect {
  t.Errorf("got [%s] expected [%s]", got, expect)
 }
}

func BenchmarkHello(b *testing.B) {
 for i := 0; i < b.N; i++ {
  hello()
 }
}

func ExampleHello() {
 hl := hello()
 fmt.Println(hl)
 // Output: hello func in package hello.
}

最后,打开终端,进入hello目录,输入go test命令并回车,可以看到如下输出:

PASS
ok  hello 0.007s

编写测试代码

Golang的测试代码位于某个包的源代码中名称以_test.go结尾的源文件里,测试代码包含测试函数、测试辅助代码和示例函数;测试函数有以Test开头的功能测试函数和以Benchmark开头的性能测试函数两种,测试辅助代码是为测试函数服务的公共函数、初始化函数、测试数据等,示例函数则是以Example开头的说明被测试函数用法的函数。

大部分情况下,测试代码是作为某个包的一部分,意味着它可以访问包中不可导出的元素。但在有需要的时候(如避免循环依赖)也可以修改测试文件的包名,如package hello的测试文件,包名可以设为package hello_test。

功能测试函数

功能测试函数需要接收*testing.T类型的单一参数t,testing.T 类型用来管理测试状态和支持格式化的测试日志。测试日志在测试执行过程中积累起来,完成后输出到标准错误输出。

下面是从Go标准库摘抄的 testing.T类型的常用方法的用法:

测试函数中的某条测试用例执行结果与预期不符时,调用t.Error()t.Errorf()方法记录日志并标记测试失败

# /usr/local/go/src/bytes/compare_test.go
func TestCompareIdenticalSlice(t *testing.T) {
 var b = []byte("Hello Gophers!")
 if Compare(b, b) != 0 {
  t.Error("b != b")
 }
 if Compare(b, b[:1]) != 1 {
  t.Error("b > b[:1] failed")
 }
}

使用t.Fatal()t.Fatalf()方法,在某条测试用例失败后就跳出该测试函数

# /usr/local/go/src/bytes/reader_test.go
func TestReadAfterBigSeek(t *testing.T) {
 r := NewReader([]byte("0123456789"))
 if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil {
  t.Fatal(err)
 }
 if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF {
  t.Errorf("Read = %d, %v; want 0, EOF", n, err)
 }
}

使用t.Skip()t.Skipf()方法,跳过某条测试用例的执行

# /usr/local/go/src/archive/zip/zip_test.go
func TestZip64(t *testing.T) {
 if testing.Short() {
  t.Skip("slow test; skipping")
 }
 const size = 1 << 32 // before the "END\n" part
 buf := testZip64(t, size)
 testZip64DirectoryRecordLength(buf, t)
}

执行测试用例的过程中通过t.Log()t.Logf()记录日志

# /usr/local/go/src/regexp/exec_test.go
func TestFowler(t *testing.T) {
 files, err := filepath.Glob("testdata/*.dat")
 if err != nil {
  t.Fatal(err)
 }
 for _, file := range files {
  t.Log(file)
  testFowler(t, file)
 }
}

使用t.Parallel()标记需要并发执行的测试函数

# /usr/local/go/src/runtime/stack_test.go
func TestStackGrowth(t *testing.T) {
 t.Parallel()
 var wg sync.WaitGroup

 // in a normal goroutine
 wg.Add(1)
 go func() {
  defer wg.Done()
  growStack()
 }()
 wg.Wait()

 // ...
}

性能测试函数

性能测试函数需要接收*testing.B类型的单一参数b,性能测试函数中需要循环b.N次调用被测函数。testing.B 类型用来管理测试时间和迭代运行次数,也支持和testing.T相同的方式管理测试状态和格式化的测试日志,不一样的是testing.B的日志总是会输出。

下面是从Go标准库摘抄的 testing.B类型的常用方法的用法:

在函数中调用t.ReportAllocs() ,启用内存使用分析

# /usr/local/go/src/bufio/bufio_test.go
func BenchmarkWriterFlush(b *testing.B) {
 b.ReportAllocs()
 bw := NewWriter(ioutil.Discard)
 str := strings.Repeat("x", 50)
 for i := 0; i < b.N; i++ {
  bw.WriteString(str)
  bw.Flush()
 }
}

通过 b.StopTimer() b.ResetTimer() b.StartTimer()来停止、重置、启动 时间经过和内存分配计数

# /usr/local/go/src/fmt/scan_test.go
func BenchmarkScanInts(b *testing.B) {
 b.ResetTimer()
 ints := makeInts(intCount)
 var r RecursiveInt
 for i := b.N - 1; i >= 0; i-- {
  buf := bytes.NewBuffer(ints)
  b.StartTimer()
  scanInts(&r, buf)
  b.StopTimer()
 }
}

调用b.SetBytes()记录在一个操作中处理的字节数

# /usr/local/go/src/testing/benchmark.go
func BenchmarkFields(b *testing.B) {
 b.SetBytes(int64(len(fieldsInput)))
 for i := 0; i < b.N; i++ {
  Fields(fieldsInput)
 }
}

通过b.RunParallel()方法和 *testing.PB类型的Next()方法来并发执行被测对象

# /usr/local/go/src/sync/atomic/value_test.go
func BenchmarkValueRead(b *testing.B) {
 var v Value
 v.Store(new(int))
 b.RunParallel(func(pb *testing.PB) {
  for pb.Next() {
   x := v.Load().(*int)
   if *x != 0 {
    b.Fatalf("wrong value: got %v, want 0", *x)
   }
  }
 })
}

测试辅助代码

测试辅助代码是编写测试代码过程中因代码重用和代码质量考虑而产生的。主要包括如下方面:

引入依赖的外部包,如每个测试文件都需要的 testing 包等:

# /usr/local/go/src/log/log_test.go:
import (
 "bytes"
 "fmt"
 "os"
 "regexp"
 "strings"
 "testing"
 "time"
)

定义多次用到的常量和变量,测试用例数据等:

# /usr/local/go/src/log/log_test.go:
const (
 Rdate   = `[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9]`
 Rtime   = `[0-9][0-9]:[0-9][0-9]:[0-9][0-9]`
 Rmicroseconds = `\.[0-9][0-9][0-9][0-9][0-9][0-9]`
 Rline   = `(57|59):` // must update if the calls to l.Printf / l.Print below move
 Rlongfile  = `.*/[A-Za-z0-9_\-]+\.go:` + Rline
 Rshortfile = `[A-Za-z0-9_\-]+\.go:` + Rline
)

// ...

var tests = []tester{
 // individual pieces:
 {0, "", ""},
 {0, "XXX", "XXX"},
 {Ldate, "", Rdate + " "},
 {Ltime, "", Rtime + " "},
 {Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "},
 {Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time
 {Llongfile, "", Rlongfile + " "},
 {Lshortfile, "", Rshortfile + " "},
 {Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile
 // everything at once:
 {Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "},
 {Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "},
}

和普通的Golang源代码一样,测试代码中也能定义init函数,init函数会在引入外部包、定义常量、声明变量之后被自动调用,可以在init函数里编写测试相关的初始化代码。

# /usr/local/go/src/bytes/buffer_test.go
func init() {
 testBytes = make([]byte, N)
 for i := 0; i < N; i++ {
  testBytes[i] = 'a' + byte(i%26)
 }
 data = string(testBytes)
}

封装测试专用的公共函数,抽象测试专用的结构体等:

# /usr/local/go/src/log/log_test.go:
type tester struct {
 flag int
 prefix string
 pattern string // regexp that log output must match; we add ^ and expected_text$ always
}

// ...

func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) {
 // ...
}

示例函数

示例函数无需接收参数,但需要使用注释的 Output: 标记说明示例函数的输出值,未指定Output:标记或输出值为空的示例函数不会被执行。

示例函数需要归属于某个 包/函数/类型/类型 的方法,具体命名规则如下:

func Example() { ... }  # 包的示例函数
func ExampleF() { ... }  # 函数F的示例函数
func ExampleT() { ... }  # 类型T的示例函数
func ExampleT_M() { ... } # 类型T的M方法的示例函数

# 多示例函数 需要跟下划线加小写字母开头的后缀
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

go doc 工具会解析示例函数的函数体作为对应 包/函数/类型/类型的方法 的用法。

测试函数的相关说明,可以通过go help testfunc来查看帮助文档。

使用 go test 工具

Golang中通过命令行工具go test来执行测试代码,打开shell终端,进入需要测试的包所在的目录执行 go test,或者直接执行go test $pkg_name_in_gopath即可对指定的包执行测试。

通过形如go test github.com/tabalt/...的命令可以执行$GOPATH/github.com/tabalt/目录下所有的项目的测试。go test std命令则可以执行Golang标准库的所有测试。

如果想查看执行了哪些测试函数及函数的执行结果,可以使用-v参数:

[tabalt@localhost hello] go test -v
=== RUN TestHello
--- PASS: TestHello (0.00s)
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
PASS
ok  hello 0.006s

假设我们有很多功能测试函数,但某次测试只想执行其中的某一些,可以通过-run参数,使用正则表达式来匹配要执行的功能测试函数名。如下面指定参数后,功能测试函数TestHello不会执行到。

[tabalt@localhost hello] go test -v -run=xxx
PASS
ok  hello 0.006s

性能测试函数默认并不会执行,需要添加-bench参数,并指定匹配性能测试函数名的正则表达式;例如,想要执行某个包中所有的性能测试函数可以添加参数-bench . 或 -bench=.。

[tabalt@localhost hello] go test -bench=.
PASS
BenchmarkHello-8  2000000   657 ns/op
ok  hello 1.993s

想要查看性能测试时的内存情况,可以再添加参数-benchmem:

[tabalt@localhost hello] go test -bench=. -benchmem
PASS
BenchmarkHello-8  2000000   666 ns/op   208 B/op   9 allocs/op
ok  hello 2.014s

参数-cover可以用来查看我们编写的测试对代码的覆盖率:


详细的覆盖率信息,可以通过-coverprofile输出到文件,并使用go tool cover来查看,用法请参考go tool cover -help

更多go test命令的参数及用法,可以通过go help testflag来查看帮助文档。

高级测试技术

IO相关测试

testing/iotest包中实现了常用的出错的Reader和Writer,可供我们在io相关的测试中使用。主要有:

触发数据错误dataErrReader,通过DataErrReader()函数创建

读取一半内容的halfReader,通过HalfReader()函数创建

读取一个byte的oneByteReader,通过OneByteReader()函数创建

触发超时错误的timeoutReader,通过TimeoutReader()函数创建

写入指定位数内容后停止的truncateWriter,通过TruncateWriter()函数创建

读取时记录日志的readLogger,通过NewReadLogger()函数创建

写入时记录日志的writeLogger,通过NewWriteLogger()函数创建

黑盒测试

testing/quick包实现了帮助黑盒测试的实用函数 Check和CheckEqual。

Check函数的第1个参数是要测试的只返回bool值的黑盒函数f,Check会为f的每个参数设置任意值并多次调用,如果f返回false,Check函数会返回错误值 *CheckError。Check函数的第2个参数 可以指定一个quick.Config类型的config,传nil则会默认使用quick.defaultConfig。quick.Config结构体包含了测试运行的选项。

# /usr/local/go/src/math/big/int_test.go
func checkMul(a, b []byte) bool {
 var x, y, z1 Int
 x.SetBytes(a)
 y.SetBytes(b)
 z1.Mul(&x, &y)

 var z2 Int
 z2.SetBytes(mulBytes(a, b))

 return z1.Cmp(&z2) == 0
}

func TestMul(t *testing.T) {
 if err := quick.Check(checkMul, nil); err != nil {
  t.Error(err)
 }
}

CheckEqual函数是比较给定的两个黑盒函数是否相等,函数原型如下:

func CheckEqual(f, g interface{}, config *Config) (err error)

HTTP测试

net/http/httptest包提供了HTTP相关代码的工具,我们的测试代码中可以创建一个临时的httptest.Server来测试发送HTTP请求的代码:

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()

res, err := http.Get(ts.URL)
if err != nil {
 log.Fatal(err)
}

greeting, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
 log.Fatal(err)
}

fmt.Printf("%s", greeting)

还可以创建一个应答的记录器httptest.ResponseRecorder来检测应答的内容:

handler := func(w http.ResponseWriter, r *http.Request) {
 http.Error(w, "something failed", http.StatusInternalServerError)
}

req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if err != nil {
 log.Fatal(err)
}

w := httptest.NewRecorder()
handler(w, req)

fmt.Printf("%d - %s", w.Code, w.Body.String())

测试进程操作行为

当我们被测函数有操作进程的行为,可以将被测程序作为一个子进程执行测试。下面是一个例子:

//被测试的进程退出函数
func Crasher() {
 fmt.Println("Going down in flames!")
 os.Exit(1)
}

//测试进程退出函数的测试函数
func TestCrasher(t *testing.T) {
 if os.Getenv("BE_CRASHER") == "1" {
  Crasher()
  return
 }
 cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
 cmd.Env = append(os.Environ(), "BE_CRASHER=1")
 err := cmd.Run()
 if e, ok := err.(*exec.ExitError); ok && !e.Success() {
  return
 }
 t.Fatalf("process ran with err %v, want exit status 1", err)
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者使用Go语言能有所帮助,如果有疑问大家可以留言交流。

(0)

相关推荐

  • 深入理解Golang的单元测试和性能测试

    前言 大家做开发的应该都知道,在开发程序中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定.本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试. go语言中自带有一个轻量级的测试框架testing和自带的go test命令来

  • 一步步教你编写可测试的Go语言代码

    第一个测试 "Hello Test!" 首先,在我们$GOPATH/src目录下创建hello目录,作为本文涉及到的所有示例代码的根目录. 然后,新建名为hello.go的文件,定义一个函数hello() ,功能是返回一个由若干单词拼接成句子: package hello func hello() string { words := []string{"hello", "func", "in", "package&q

  • 一步步教大家编写酷炫的导航栏js+css实现

    一步一步的学习制作导航栏,文章末尾再做个综合页面,分享给大家一个炫酷的导航栏供大家参考,具体内容如下 1.当前页面高亮显示的导航栏 首先是HTML代码,很简单,ul+li实现菜单 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>导航栏一</title> </head> <body>

  • 教你编写SQLMap的Tamper脚本过狗

    目录 测试环境 最新版某狗 测试方法 bypass and order by union select 加个换行试试 获取表字段 编写tamper 测试环境 最新版某狗 测试方法 安全狗其实是比较好绕的WAF,绕过方法很多,但这里我们就用一种:注释混淆 一招鲜吃遍天 注释混淆,其实就是在敏感位置添加垃圾字符注释,常用的垃圾字符有/.!.*.%等 这里再解释一下内联注释,因为后面要用到: MySQL内联注释: /*!xxxxxxx*/ !后面的语句会当作SQL语句直接执行 但是如果!后面跟着MyS

  • 用C语言winform编写渗透测试工具实现SQL注入功能

    目录 用C语言winform编写渗透测试工具使SQL注入 一.SQL注入 二.实现步骤 三.代码实现 四.软件使用步骤 用C语言winform编写渗透测试工具使SQL注入 一.SQL注入 原理: SQL注入是指攻击者在Web应用程序中事先定义好的查询语句的结尾加上额外的SQL语句,这些一般都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作.(危害:盗取网站敏感信息.绕过验证登录网站后台.借助数据库的存储过程进行权限提升等操作).造成的原因是程序员在编写Web程序时,没有对浏览

  • 教你编写Windows的VBScript与Mac的AppleSCript脚本解放双手

    目录 一.Windows 篇 —— VBScript 1.效果图 2.VBS 简介 3.代码实现 4.代码详解 5.其他语法 6.附:VBS 特殊字符表格 二.Mac 篇 —— AppleScript 1.效果图 2.代码实现 3.代码详解 一.Windows 篇 —— VBScript 最近发现 windows 上有一个好玩的东西,叫做 VBScript,可以用来自动执行一些操作. 1.效果图 先来看下最终效果吧! 以上就是一个简单的 VBS 脚本,运行时自动打开 Chrome 浏览器,然后自

  • 教你编写bat脚本Windows批处理

    目录 常见用途 bat命令速查 常用参数 echo 注释 Rem 使用::声明 dir :获取目录内容 > :将输出重定向到文件 变量 命令行参数 set命令 字符串 数值 局部变量 vs 全局变量 环境变量输出 字符串 创建 空字符串 字符串拼接 字符串长度 转int 截取 右对齐 删除字符串 删除两端(保留中间) 删除空格:= 替换 数组 创建 访问 修改 迭代 数组长度 结构体 if语句 if defined :变量是否存在 if exists:文件是否存在 if errorlevel:测

  • C语言杨氏矩阵实例教你编写

    目录 一.杨氏矩阵是什么 二.编写步骤 三.程序的改进 四.总结 一.杨氏矩阵是什么 一个数字矩阵,矩阵的每一行从左到右一次递增,矩阵从上到下递增,在这样的矩阵中查找一个数字是否存在.时间复杂度小于O(N). 二.编写步骤 1.首先给出一个三乘三的矩阵,我们想找到数字7是否在这个数组之中,如图所示. 2. 我们给出的方法是一种从右上角开始,另一种是从左下角开始.比如从右上角开始,编写一个函数. 3. 函数的编写.从右上角开始,将右上角数字与数字7比较,如果数字7大于右上角的数字(3)(因为第一行

  • C++编写生成不重复的随机数代码

    C++编写生成不重复的随机数代码 vector<int> getRandom(int total) { srand((int)time(NULL)); std::vector<int> input = *new std::vector<int>(); for (int i = 0; i < total; i++) { input.push_back(i); } vector<int> output = *new vector<int>();

  • 手把手教你用python抢票回家过年(代码简单)

    首先看看如何快速查看剩余火车票? 当你想查询一下火车票信息的时候,你还在上12306官网吗?或是打开你手机里的APP?下面让我们来用Python写一个命令行版的火车票查看器, 只要在命令行敲一行命令就能获得你想要的火车票信息!如果你刚掌握了Python基础,这将是个不错的小练习. 接口设计 一个应用写出来最终是要给人使用的,哪怕只是给你自己使用.所以,首先应该想想你希望怎么使用它?让我们先给这个小应用起个名字吧,既然及查询票务信息,那就叫它tickets好了.我们希望用户只要输入出发站,到达站以

  • 基于Docker+Selenium Grid的测试技术应用示例代码

    Selenium Grid介绍 尽管在未来将会推出的Selenium 4.0版本中对Selenium Grid的一些新特性进行了说明,但是目前来看官方并没有太多详细文档供大家参考,所以本书中仍结合目前被广泛使用的Selenium Grid 版本进行讲解. 正如其官网对Selenium Grid的描述,它是一个智能代理服务器,允许Selenium测试将命令路由到远程Web浏览器实例.其目的是提供一种在多台计算机上并行运行测试的简便方法.使用Selenium Grid,一台服务器充当将JSON格式的

随机推荐