• GO单元测试


    作为golang人我们一定要养成写单元测试的习惯,这样才能保证我们交付质量,也称为TDD(Test Driven Development)测试驱动开发

    什么是单元测试

    • 单元是应用的最小可测试部件,如函数和对象的方法
    • 单元测试是软件开发中对最小单位进行正确性检验的测试工作

    为什么进行单元测试

    • 保证变更/重构的正确性,特别是在一些频繁变动和多人合作开发的项目中
    • 简化调试过程:可以轻松的让我们知道哪一部分代码出了问题
    • 单测最好的文档:在单测中直接给出具体接口的使用方法,是最好的实例代码

    单元测试用例编写的原则

    • 单一原则:一个测试用例只负责一个场景
    • 原子性:结果只有两种情况:Pass/Fail
    • 优先要核心组件和逻辑的测试用例
    • 高频使用库,util,重点覆盖这些

    单测用例约定

    • 文件名必须要xx_test.go命名

      • 文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数

        类型 格式 作用
        基准函数 函数名前缀为Benchmark 测试函数的性能
        测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
        示例函数 函数名前缀为Example 为文档提供示例文档
    • 测试方法必须是Testxx开头

    • 方法中的参数 必须是t *testing.T

    • 测试文件和被测试文件必须在一个包中

    其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

    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
    

    动手环节

    简单testing

    // xx_tets.go
    package models
    
    import (
    	"testing"
    )
    
    func Add(a, b int) int {
    	return a + b
    }
    func TestAdd(t *testing.T) {
    	a := 10
    	b := 20
    	want := 30
    
    	actual := Add(a, b)
    	if want != actual {
    		t.Errorf("[Add 函数参数:%d %d][期望:%d][实际:%d]", a, b, want, actual)
    	}
    }
    
    // **执行go test -v .**
    // **=== RUN   TestAdd**
    // **--- PASS: TestAdd (0.00s)**
    // **PASS
    // ok** 
    

    运行过程:go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

    切片单元测试

    (1/5)写一个Split函数(main.go)

    package main
    
    import (
    	"strings"
    )
    
    func Split(s, sep string) (result []string) {
    	i := strings.Index(s, sep)
    	for i > -1 {
    		if i == 0 { // 如果划分是开头的第一个则进
    			s = s[len(sep):]
    		} else {
    			result = append(result, s[:i])
    			s = s[i+len(sep):]
    		}
    		i = strings.Index(s, sep)
    	}
    	result = append(result, s)
    	return
    }
    

    (2/5)同样的包下,写一个main_test.go测试文件

    package main
    
    import (
    	"reflect"
    	"testing"
    )
    
    func TestSplit(t *testing.T) {
    	got := Split("a:b:c", ":")         // 程序输出的结果
    	want := []string{"a", "b", "c"}    // 期望的结果
    	if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
    		t.Errorf("expected:%v, got:%v", want, got) // 测试失败输出错误提示
    	}
    }
    

    (3/5)文件结构

    $ ls
    main.go  main_test.go
    

    (4/5)测试通过例子

    $ go test
    PASS
    ok      test    0.002s
    
    $ go test -v 
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    PASS
    ok      test    0.001s
    

    (5/5)测试失败例子

    $ go test .
    --- FAIL: TestSplit (0.00s)
        main_test.go:12: expected:[a b d], got:[a b c]
    FAIL
    exit status 1
    FAIL    test    0.001s
    
    $ go test -v .
    === RUN   TestSplit
        main_test.go:12: expected:[a b d], got:[a b c]
    --- FAIL: TestSplit (0.00s)
    FAIL
    exit status 1
    FAIL    test    0.001s
    

    测试组

    添加更多的测试用例,保证在交付前把缺陷解决完,添加TestMoreSplit函数

    func TestMoreSplit(t *testing.T) {
    	type test struct {
    		input string
    		sep   string
    		want  []string
    	}
    
    	// 插入测试数据
    	tests := []test{
    		{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
    		{input: ":a:b:c", sep: ":", want: []string{"a", "b", "c"}},
    		{input: "abc", sep: ":", want: []string{"abc"}},
    		{input: "abcd", sep: "bc", want: []string{"a", "d"}},
    		{input: "云原生", sep: "云", want: []string{"原生"}},
    	}
    
    	// 遍历slice
    	for _, d := range tests {
    		got := Split(d.input, d.sep)
    		if !reflect.DeepEqual(got, d.want) {
    			t.Errorf("expected:%v, got:%v", d.want, got)
    		}
    	}
    }
    

    go test -v .结果

    $ go test -v .
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    ok      test    0.002s
    

    子测试

    自定义子测试名称

    package main
    
    import (
    	"testing"
    )
    
    func Add(a, b int) int {
    	return a + b
    }
    func TestAdd(t *testing.T) {
    	t.Run("正数", func(t  *testing.T) {
    		if Add(10, 20) != 30 {
    			t.Fatal("add.zhengshu.error")
    		}
    	})
    	
    	t.Run("负数", func(t *testing.T) {
    		if Add(-20, 10) != -10 {
    			t.Fatal("add.fushu.error")
    		}
    	})
    }
    
    // go test -v .     
    // === RUN   TestAdd
    // === RUN   TestAdd/正数
    // === RUN   TestAdd/负数
    // --- PASS: TestAdd (0.00s)
    //     --- PASS: TestAdd/正数 (0.00s)
    //     --- PASS: TestAdd/负数 (0.00s)
    // PASS
    

    还可以这样写

    package main
    
    import (
    	"testing"
    )
    
    func Add(a, b int) int {
    	return a + b
    }
    func TestAdd(t *testing.T) {
    	type test struct {
    		num1 int
    		num2 int
    		want int
    	}
    	tests := map[string]test{
    		"正数": {num1: 10, num2: 20, want: 30},
    		"负数": {num1: -20, num2: 10, want: -10},
    	}
    	for name, i := range tests {
    		t.Run(name, func(t *testing.T) {
    			got := Add(i.num1, i.num2)
    			if got != i.want {
    				t.Fatalf("%v error, 期望: %d, 实际: %d", name, i.want, got)
    			}
    		})
    	}
    }
    
    // $ go test -v .
    // === RUN   TestAdd
    // === RUN   TestAdd/正数
    // === RUN   TestAdd/负数
    // --- PASS: TestAdd (0.00s)
    //     --- PASS: TestAdd/正数 (0.00s)
    //     --- PASS: TestAdd/负数 (0.00s)
    // PASS
    // ok      test    (cached)
    

    执行某一个函数

    $ go test -run=TestSplit -v .
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    PASS
    ok      test    0.001s
    
    // 模糊测试
    $ go test -run=TestMore.* -v .
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    ok      test    0.001s
    
    // 执行子测试函数
    $ go test -run=TestAdd/正数 -v .
    === RUN   TestAdd
    === RUN   TestAdd/正数
    --- PASS: TestAdd (0.00s)
        --- PASS: TestAdd/正数 (0.00s)
    PASS
    ok      test    0.002s
    

    跳过某个测试函数

    
    

    go test -short

    测试覆盖率

    用于统计目标包有百分之多少的代码参与了单测

    $ go test -v -cover
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    coverage: 100.0% of statements
    ok      test    0.002s
    

    -coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件中

    $ go test -v -cover -coverprofile=split.out
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestMoreSplit
    --- PASS: TestMoreSplit (0.00s)
    PASS
    coverage: 100.0% of statements
    ok      test    0.001s
    
    // 该命令会打开本地的浏览器窗口生成一个HTML报告
    $ go tool cover -html=split.out
    

    表驱动测试 table-driven tests

    // cal.go
    package main
    
    func Add(x int) (res int) {
    	res = x + 2
    	return res
    }
    
    // cal_test.go
    package main
    
    import (
    	"testing"
    
    	"github.com/stretchr/testify/assert"
    )
    
    func TestAdd(t *testing.T) {
    	assert.Equal(t, Add(5), 7, "they should be equal")
    }
    
    func TestCal(t *testing.T) {
    	ass := assert.New(t)
    	var tests = []struct {
    		input    int
    		expected int
    	}{
    		{2, 4},
    		{1, 3},
    		{-2, 0},
    		{-6, -4},
    		{6, 8},
    	}
    	for _, test := range tests {
    		ass.Equal(Add(test.input), test.expected)
    	}
    }
    

    GoConvey测试框架

    // student.go
    package models
    
    import "fmt"
    
    type Student struct {
    	Name      string
    	ChiScore  int
    	EngScore  int
    	MathScore int
    }
    
    func NewStudent(name string) (*Student, error) {
    	if name == "" {
    		return nil, fmt.Errorf("name为空")
    	}
    	return &Student{
    		Name: name,
    	}, nil
    }
    
    func (s *Student) GetAvgScore() (int, error) {
    	score := s.ChiScore + s.EngScore + s.MathScore
    	if score == 0 {
    		return 0, fmt.Errorf("全部为0分")
    	}
    	return score / 3, nil
    }
    
    // student_test.go
    package models
    
    import (
    	"testing"
    
    	. "github.com/smartystreets/goconvey/convey"
    )
    
    func TestNewStudent(t *testing.T) {
    	Convey("start test new", t, func() {
    		stu, err := NewStudent("")
    		Convey("空的name初始化报错", func() {
    			So(err, ShouldBeError) // 判断err
    		})
    		Convey("stu对象为nil", func ()  {
    			So(stu, ShouldBeNil) // 判断stu
    		})
    	})
    }
    
    // go test -v .
    // === RUN   TestNewStudent
    
    //   start test new
    //     空的name初始化报错 .
    //     stu对象为nil .
    
    // 2 total assertions
    
    // --- PASS: TestNewStudent (0.00s)
    // PASS
    
    // 进阶
    package models
    
    import (
    	"testing"
    
    	. "github.com/smartystreets/goconvey/convey"
    )
    
    func TestNewStudent(t *testing.T) {
    	Convey("start test new", t, func() {
    		stu, err := NewStudent("")
    		Convey("空的name初始化报错", func() {
    			So(err, ShouldBeError)
    		})
    		Convey("stu对象为nil", func() {
    			So(stu, ShouldBeNil)
    		})
    	})
    }
    
    func TestScore(t *testing.T) {
    	stu, _ := NewStudent("xx")
    	Convey("不设置分数可能出错", t, func() {
    		sc, err := stu.GetAvgScore()
    		Convey("获取分数出错了", func() {
    			So(err, ShouldBeError)
    		})
    		Convey("分数为0", func() {
    			So(sc, ShouldEqual, 0)
    		})
    	})
    	Convey("正常情况", t, func() {
    		stu.ChiScore = 60
    		stu.EngScore = 70
    		stu.MathScore = 80
    		score, err := stu.GetAvgScore()
    		Convey("获取分数出错了", func() {
    			So(err, ShouldBeNil)
    		})
    		Convey("平均分大于60", func() {
    			So(score, ShouldBeGreaterThan, 60)
    		})
    	})
    }
    
    // go test -v .
    // === RUN   TestNewStudent
    
    //   start test new
    //     空的name初始化报错 .
    //     stu对象为nil .
    
    // 2 total assertions
    
    // --- PASS: TestNewStudent (0.00s)
    // === RUN   TestScore
    
    //   不设置分数可能出错
    //     获取分数出错了 .
    //     分数为0 .
    
    // 4 total assertions
    
    //   正常情况
    //     获取分数出错了 .
    //     平均分大于60 .
    
    // 6 total assertions
    
    // --- PASS: TestScore (0.00s)
    // PASS
    // ok
    

    安装图形化goconvey

    // 安装二进制,添加环境变量
    go get github.com/smartystreets/goconvey
    
    // 运行
    goconvey
    

    testify

    // cal.go
    package main
    
    func Add(x int) (res int) {
    	res = x + 2
    	return res
    }
    
    // cal_test.go 
    package main
    
    import (
    	"testing"
    
    	"github.com/stretchr/testify/assert"
    )
    
    func TestAdd(t *testing.T) {
    	assert.Equal(t, Add(5), 7, "they should be equal")
    }
    

    testify/mock数据

    https://github.com/euclidr/testingo
    
  • 相关阅读:
    《面向对象编程》c++ primer 第15章
    extern的作用(综合网络)
    C程序内存区域分配(5个段作用)
    鼓舞自己的名言
    快速指数算法 和 求逆元算法
    HP Xeon 55xx上GPU的带宽问题
    Ubuntu12.04 安装ibusfbterm0.9.1
    Win7下读写Ext2/Ext3/Ext4文件系统
    fbv安装 为console添加背景图片
    CentOS6/7安装fcitx4.2.5
  • 原文地址:https://www.cnblogs.com/Otiger/p/16221876.html
Copyright © 2020-2023  润新知