ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

GO单元测试

2022-05-04 18:33:35  阅读:156  来源: 互联网

标签:func testing 单元测试 test TestAdd go PASS 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

标签:func,testing,单元测试,test,TestAdd,go,PASS,GO
来源: https://www.cnblogs.com/Otiger/p/16221876.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有