Go语言——测试

Go语言作为一门21世纪的语言,自然提供了测试工具。这篇文章就介绍一下如何对Go语言所写的程序进行测试。
Go语言测试命令主要就这一条go test
测试源代码文件都以*_test.go为后缀,这些文件不会被编译成二进制文件。

单元测试

单元测试的测试函数以Test为函数的前缀名,然后参数为*testing.T类型,比如:

func TestABC(t *testing.T) {
}

以下例子来源李文周的博客
我们对split.go这个文件进行测试

// split/split.go

package split

import "strings"

// split package with a single split function.

// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
	result = make([]string, 0, strings.Count(s, sep)+1)
	i := strings.Index(s, sep)

	for i > -1 {
		result = append(result, s[:i])
		s = s[i+len(sep):]
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}

split_test.go

func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
	type test struct {
		input string
		sep   string
		want  []string
	}

	tests := map[string]test{
		"simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
		"wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
		"more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
		"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
	}

	teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
	defer teardownTestCase(t)            // 测试之后执行testdoen操作

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
			defer teardownSubTest(t)           // 测试之后执行testdoen操作
			got := Split(tc.input, tc.sep)
			if !reflect.DeepEqual(got, tc.want) {
				t.Errorf("excepted:%#v, got:%#v", tc.want, got)
			}
		})
	}
}

执行go test -v可以看到下面的效果

write setup code here...
=== RUN   TestSplit
    TestSplit: split_test.go:77: 如有需要在此执行:测试之前的setup
=== RUN   TestSplit/simple
    TestSplit/simple: split_test.go:85: 如有需要在此执行:子测试之前的setup
    TestSplit/simple: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/wrong_sep
    TestSplit/wrong_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
    TestSplit/wrong_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/more_sep
    TestSplit/more_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
    TestSplit/more_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
=== RUN   TestSplit/leading_sep
    TestSplit/leading_sep: split_test.go:85: 如有需要在此执行:子测试之前的setup
    TestSplit/leading_sep: split_test.go:87: 如有需要在此执行:子测试之后的teardown
    TestSplit: split_test.go:79: 如有需要在此执行:测试之后的teardown
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/wrong_sep (0.00s)
    --- PASS: TestSplit/more_sep (0.00s)
    --- PASS: TestSplit/leading_sep (0.00s)
=== RUN   TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
=== RUN   ExampleSplit
--- PASS: ExampleSplit (0.00s)
PASS
write teardown code here...
ok      go_learning/split       1.408s

现在来分析一下每个语句做什么
我们只需要关注teardownTestCase下面的内容,这行以上的只是用于存储测试用例

teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
defer teardownTestCase(t)            // 测试之后执行testdoen操作

这两句一个是在测试执行前执行,一个是之后执行,我们看一下setupTestCase(t)是如何编写的

func setupTestCase(t *testing.T) func(t *testing.T) {
	t.Log("如有需要在此执行:测试之前的setup")
	return func(t *testing.T) {
		t.Log("如有需要在此执行:测试之后的teardown")
	}
}

t.Errorf("excepted:%#v, got:%#v", tc.want, got)是将错误信息打印出来,类似的还有

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

t.Run(name, func(t *testing.T){})是用来执行子测试的
通过go test -v -run=RegExp可以用来只执行匹配的函数
go test -cover -coverprofile=可以用来查看测试代码覆盖率

性能测试

进行性能测试的函数以Benchmark_为前缀

func BenchmarkName(b *testing.B){}

b有如下的方法:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()

执行命令go test -bench=.还可以加上-benchmem来测试内存分配

func BenchmarkSplitParallel(b *testing.B) {
	//go test -bench=. -cpu1
	b.SetParallelism(2)
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Split("沙河有沙又有河", "沙")
		}
	})
}

b.RunParallel(func(b *testing.B){})是以并行的方式来进行测试,SetParallelism()是设置cpu数

性能优化

go语言标准库中自带可以进行性能分析的工具

runtime/pprof
net/http/pprof

pprof开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取格格函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。注意只有在需要性能分析的时候才应该引入这些工具,因为它们同样会消耗性能
开启CPU性能分析
pprof.StartCPUProfile(w io.Writer)
关闭CPU性能分析
pprof.StopCPUProfile()
得到采样数据之后,使用go tool pprof工具进行CPU性能分析。
内存性能分析
pprof.WriteHeapProfile(w io.Writer)
下面给个完整的例子

package main

import (
	"flag"
	"fmt"
	"os"
	"runtime/pprof"
	"time"
)

// 一段有问题的代码
func logicCode() {
	var c chan int
	for {
		select {
		case v := <-c:
			fmt.Printf("recv from chan, value:%v\n", v)
		default:

		}
	}
}

func main() {
	var isCPUPprof bool
	var isMemPprof bool

	flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
	flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
	flag.Parse()

	if isCPUPprof {
		file, err := os.Create("./cpu.pprof")
		if err != nil {
			fmt.Printf("create cpu pprof failed, err:%v\n", err)
			return
		}
		pprof.StartCPUProfile(file)
		defer pprof.StopCPUProfile()
	}
	for i := 0; i < 8; i++ {
		go logicCode()
	}
	time.Sleep(20 * time.Second)
	if isMemPprof {
		file, err := os.Create("./mem.pprof")
		if err != nil {
			fmt.Printf("create mem pprof failed, err:%v\n", err)
			return
		}
		pprof.WriteHeapProfile(file)
		file.Close()
	}
}

其他

示例函数

示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。

func ExampleSplit() {
	fmt.Println(split.Split("a:b:c", ":"))
	fmt.Println(split.Split("沙河有沙又有河", "沙"))
	// Output:
	// [a b c]
	// [ 河有 又有河]
}

执行go test -run Example

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值