Contents

Go测试

单元测试

普通测试

规则

  • 测试文件以 _test.go 为后缀;测试函数以 Test 为前缀

实践

  • 新建main_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
	"fmt"
	"math/rand"
	"testing"
	"time"
)

func FbnqDp(n int) (s int) {
	dp := make([]int, n+1)
	dp[1] = 1

	for i := 2; i < n+1; i++ {
		dp[i] = dp[i-2] + dp[i-1]
	}

	return dp[n]
}

func Fbnq(n int) (s int) {
	if n <= 2 {
		return 1
	}
	return Fbnq(n-1) + Fbnq(n-2)
}

func TestFbnqDp(t *testing.T) {
	type args struct {
		n int
	}
	type fbnqDpTest struct {
		name  string
		args  args
		wantS int
	}
	tests := []fbnqDpTest{}
	rand.Seed(time.Now().Unix())
	for i := 0; i < 10; i++ {
		x := rand.Intn(30) + 1
		tests = append(tests, fbnqDpTest{name: fmt.Sprintf("TestFbnqDp(%d)", x), args: args{n: x}, wantS: Fbnq(x)})
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if gotS := FbnqDp(tt.args.n); gotS != tt.wantS {
				t.Errorf("FbnqDp() = %v, want %v", gotS, tt.wantS)
			}
		})
	}
}
  • 执行测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
➜  p3 go test -gcflags=all=-l  -v main_test.go                
=== RUN   TestFbnqDp
=== RUN   TestFbnqDp/TestFbnqDp(6)
=== RUN   TestFbnqDp/TestFbnqDp(2)
=== RUN   TestFbnqDp/TestFbnqDp(8)
=== RUN   TestFbnqDp/TestFbnqDp(1)
=== RUN   TestFbnqDp/TestFbnqDp(27)
=== RUN   TestFbnqDp/TestFbnqDp(22)
=== RUN   TestFbnqDp/TestFbnqDp(26)
=== RUN   TestFbnqDp/TestFbnqDp(27)#01
=== RUN   TestFbnqDp/TestFbnqDp(19)
=== RUN   TestFbnqDp/TestFbnqDp(3)
--- PASS: TestFbnqDp (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(6) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(2) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(8) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(1) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(27) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(22) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(26) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(27)#01 (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(19) (0.00s)
    --- PASS: TestFbnqDp/TestFbnqDp(3) (0.00s)
PASS
ok      command-line-arguments  0.007s
  • 参数说明: -v 表示显示每个用例的测试结果; -gcflags=all=-l 表示禁止 内联 ,详见 Testing flags

  • 执行测试时,可以指定文件,比如上面的命令中,main_test.go;如果被测试函数不在该文件中,不能指定文件

基准测试

规则

  • 测试文件以 _test.go 为后缀;测试函数以 Benchmark 为前缀

实践

  • main_test.go添加代码
1
2
3
4
5
6
7
func BenchmarkFbnq(b *testing.B) {
	Fbnq(30)
}

func BenchmarkFbnqDp(b *testing.B) {
	FbnqDp(30)
}
  • 执行测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
➜  p3 go test -bench Fbnq -v main_test.go
# ……
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkFbnq
BenchmarkFbnq-4         1000000000               0.009437 ns/op
BenchmarkFbnqDp
BenchmarkFbnqDp-4       1000000000               0.0000027 ns/op
PASS
ok      command-line-arguments  0.052s
  • 常用参数: -benchtime=50x 表示单次测试函数执行次数,50次; -benchtime=5s 表示单次测试的时间为5s; -bench Fbnq 表示函数名称中含有 Fbnq-count=1 表示执行几次完整的测试; -benchmem 表示内存使用情况

net测试

  • 新建 net_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
	"io/ioutil"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

func handleError(t *testing.T, err error) {
	t.Helper()
	if err != nil {
		t.Fatal("failed", err)
	}
}

func TestConn(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	handleError(t, err)
	defer ln.Close()

	http.HandleFunc("/hello", helloHandler)
	go http.Serve(ln, nil)

	resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
	handleError(t, err)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	handleError(t, err)

	if string(body) != "hello world" {
		t.Fatal("expected hello world, but got", string(body))
	}
}
func TestConn1(t *testing.T) {
	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	// 获取一个ResponseWriter,主要是为了测试 helloHandler 函数
	w := httptest.NewRecorder()
	helloHandler(w, req)
	bytes, _ := ioutil.ReadAll(w.Result().Body)

	if string(bytes) != "hello world" {
		t.Fatal("expected hello world, but got", string(bytes))
	}
}
  • 执行测试
1
2
3
4
5
6
7
➜  p14 go test -v net_test.go
=== RUN   TestConn
--- PASS: TestConn (0.00s)
=== RUN   TestConn1
--- PASS: TestConn1 (0.00s)
PASS
ok      command-line-arguments  0.005s

testify

安装

1
go get github.com/stretchr/testify

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
)

type _Suite struct {
	suite.Suite
}

// SetupSuite() 和 TearDownSuite() 全局只会执行一次
// SetupTest() TearDownTest() BeforeTest() AfterTest() 对套件中的每个测试执行一次
func (s *_Suite) AfterTest(suiteName, testName string) {
	fmt.Printf("AferTest: suiteName=%s,testName=%s\n", suiteName, testName)
}

func (s *_Suite) BeforeTest(suiteName, testName string) {
	fmt.Printf("BeforeTest: suiteName=%s,testName=%s\n", suiteName, testName)
}

// SetupSuite() 仅执行一次
func (s *_Suite) SetupSuite() {
	fmt.Printf("SetupSuite() ...\n")
}

// TearDownSuite() 仅执行一次
func (s *_Suite) TearDownSuite() {
	fmt.Printf("TearDowmnSuite()...\n")
}

func (s *_Suite) SetupTest() {
	fmt.Printf("SetupTest()... \n")
}

func (s *_Suite) TearDownTest() {
	fmt.Printf("TearDownTest()... \n")
}
func FbnqDp(n int) (s int) {
	dp := make([]int, n+1)
	dp[1] = 1

	for i := 2; i < n+1; i++ {
		dp[i] = dp[i-2] + dp[i-1]
	}

	return dp[n]
}

func Fbnq(n int) (s int) {
	if n <= 2 {
		return 1
	}
	return Fbnq(n-1) + Fbnq(n-2)
}

func (s *_Suite) TestFbnq() {
	fmt.Printf("TestFbnq()... \n")
	ret := Fbnq(30)
	assert.Equal(s.T(), ret, 832040)
}

func (s *_Suite) TestFbnqDp() {
	fmt.Printf("TestFbnqDp()... \n")
	ret := FbnqDp(30)
	assert.Equal(s.T(), ret, 832040)
}

// 让 go test 执行测试
func TestAll(t *testing.T) {
	suite.Run(t, new(_Suite))
}
  • 执行测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
➜  p15 go test -v tf_test.go -run TestAll
=== RUN   TestAll
SetupSuite() ...
=== RUN   TestAll/TestFbnq
SetupTest()... 
BeforeTest: suiteName=_Suite,testName=TestFbnq
TestFbnq()... 
AferTest: suiteName=_Suite,testName=TestFbnq
TearDownTest()... 
=== RUN   TestAll/TestFbnqDp
SetupTest()... 
BeforeTest: suiteName=_Suite,testName=TestFbnqDp
TestFbnqDp()... 
AferTest: suiteName=_Suite,testName=TestFbnqDp
TearDownTest()... 
TearDowmnSuite()...
--- PASS: TestAll (0.01s)
    --- PASS: TestAll/TestFbnq (0.01s)
    --- PASS: TestAll/TestFbnqDp (0.00s)
PASS
ok      command-line-arguments  0.013s

Ginkgo

安装

1
2
go install github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...

使用示例

  • 编写测试用例 books/books.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package books

type Book struct {
	Title  string
	Author string
	Pages  int
}

func (b *Book) CategoryByLength() string {

	if b.Pages >= 300 {
		return "NOVEL"
	}

	return "SHORT STORY"
}
  • 生成测试入口文件
1
2
3
4
5
6
7
➜  books ginkgo bootstrap                 
Generating ginkgo test suite bootstrap for books in:
        books_suite_test.go

Ginkgo 2.0 is coming soon!
==========================
# ……
  • 生成测试文件
1
2
3
4
5
6
7
8
  books ginkgo generate books
Generating ginkgo test for Books in:
  books_test.go


Ginkgo 2.0 is coming soon!
==========================
# ……
  • 编写测试文件
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package books_test

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"

	b "p15/books"
)

var _ = Describe("Book", func() {
	var (
		longBook  b.Book
		shortBook b.Book
	)

	BeforeEach(func() {
		longBook = b.Book{
			Title:  "Les Miserables",
			Author: "Victor Hugo",
			Pages:  1488,
		}

		shortBook = b.Book{
			Title:  "Fox In Socks",
			Author: "Dr. Seuss",
			Pages:  24,
		}
	})

	Describe("Categorizing book length", func() {
		Context("With more than 300 pages", func() {
			It("should be a novel", func() {
				Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
			})
		})

		Context("With fewer than 300 pages", func() {
			It("should be a short story", func() {
				Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY"))
			})
		})
	})
})
  • 执行测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
➜  books go test -v p15/books/         
=== RUN   TestBooks
Running Suite: Books Suite
==========================
Random Seed: 1657256395
Will run 2 of 2 specs

••
Ran 2 of 2 Specs in 0.001 seconds
SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestBooks (0.00s)
PASS
ok      p15/books       0.010s

mock测试

安装

1
go get -u github.com/golang/mock/gomock

使用示例

  • 测试用例: biz/user.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package biz

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	ID       int64  `gorm:"primarykey"`
	Phone    string `gorm:"index:idx_phone;unique;type:varchar(11) comment '手机号码,用户唯一标识';not null"`
	Pwd      string `gorm:"type:varchar(100);not null "`
	Nickname string `gorm:"type:varchar(25) comment '用户昵称'"`
	Uname    string `gorm:"type:varchar(25) comment '用户名'"`
}

// 注意这一行新增的 mock 数据的命令 "p15/biz" 必须写全否则生成的测试文件引入包报错
//go:generate mockgen -destination=../mock/mrepo/user.go -package=mrepo "p15/biz" UserRepo
type UserRepo interface {
	CreateUser(*User) (*User, error)
	GetUserById(id int64) (*User, error)
}

type userRepo struct {
	db *gorm.DB
}

func NewUserRepo() *userRepo {
	mysqlSource := "dywily:q123456we@tcp(127.0.0.1:3306)/first?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(mysqlSource), &gorm.Config{})

	if err != nil {
		panic("failed to connect database")
	}

	return &userRepo{db: db}
}

type UserUsecase struct {
	repo UserRepo
}

func NewUserUsecase(repo UserRepo) *UserUsecase {
	return &UserUsecase{repo: repo}
}

func (uc *UserUsecase) Create(u *User) (*User, error) {
	return uc.repo.CreateUser(u)
}

func (uc *UserUsecase) UserById(id int64) (*User, error) {
	return uc.repo.GetUserById(id)
}

func (r *userRepo) CreateUser(u *User) (*User, error) {
	var user User
	// 验证是否已经创建
	result := r.db.Where(&User{Phone: u.Phone}).First(&user)
	if result.RowsAffected == 1 {
		panic("用户已存在")
	}

	user.ID = u.ID
	user.Uname = u.Uname
	user.Phone = u.Phone
	user.Nickname = u.Nickname
	user.Pwd = u.Pwd
	res := r.db.Create(&user)
	if res.Error != nil {
		panic(res.Error.Error())
	}

	return &User{
		ID:       user.ID,
		Phone:    user.Phone,
		Pwd:      user.Pwd,
		Nickname: user.Nickname,
		Uname:    user.Uname,
	}, nil
}

func (r *userRepo) GetUserById(Id int64) (*User, error) {
	var user User
	result := r.db.Where(&User{ID: Id}).First(&user)
	if result.Error != nil {
		return nil, result.Error
	}

	if result.RowsAffected == 0 {
		panic("用户不存在")
	}
	return &user, nil
}
  • 生产mock文件
1
mockgen -destination=../mock/mrepo/user.go -package=mrepo "p15/biz" UserRepo

或者

https://static.duan1v.top/images/739b9926034d7e1172c6f6e0961a47d2.png
go-test
  • 编写测试文件 biz/biz_suite_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package biz_test

import (
	"testing"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

func TestBiz(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "biz user test")
}

var ctl *gomock.Controller
var cleaner func()

var _ = BeforeEach(func() {
	ctl = gomock.NewController(GinkgoT())
	cleaner = ctl.Finish
})
var _ = AfterEach(func() {
	// remove any mocks
	cleaner()
})
  • 编写测试文件 biz/user_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package biz_test

import (
	"p15/mock/mrepo"

	"p15/biz"

	"github.com/golang/mock/gomock"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("UserUsecase", func() {
	var userCase *biz.UserUsecase
	var mUserRepo *mrepo.MockUserRepo
	info := &biz.User{}
	BeforeEach(func() {
		mUserRepo = mrepo.NewMockUserRepo(ctl)
		userCase = biz.NewUserUsecase(mUserRepo)
		info = &biz.User{
			ID:       3,
			Phone:    "15866547585",
			Pwd:      "admin123456",
			Nickname: "hahaha",
			Uname:    "hahaha",
		}
	})

	It("Create", func() {
		// 这里不会在数据库中创建
		mUserRepo.EXPECT().CreateUser(gomock.Any()).Return(info, nil)
		l, err := userCase.Create(info)
		Ω(err).ShouldNot(HaveOccurred())
		Ω(err).ToNot(HaveOccurred())
		Ω(l.ID).To(Equal(int64(3)))
		Ω(l.Phone).To(Equal("15866547585"))
	})

	It("UserById", func() {
		mUserRepo.EXPECT().GetUserById(gomock.Any()).Return(info, nil)
		user, err := userCase.UserById(3)
		Ω(err).ShouldNot(HaveOccurred())
		Ω(user.Phone).Should(Equal("15866547585"))
	})
})

var _ = Describe("User", func() {
	var ro biz.UserRepo
	var uD *biz.User
	BeforeEach(func() {
		ro = biz.NewUserRepo()
		uD = &biz.User{
			ID:       4,
			Phone:    "18655864568",
			Pwd:      "admin123",
			Nickname: "hahaha",
			Uname:    "hahaha",
		}
	})
	It("CreateUser", func() {
		// 这里会真的在数据库中创建
		ro.CreateUser(uD)
	})
	It("GetUserById", func() {
		user, err := ro.GetUserById(4)
		Ω(err).ShouldNot(HaveOccurred())
		Ω(user.Phone).Should(Equal("18655864568"))
	})
})
  • 目录结构
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
➜  p15 tree .
.
├── biz
│   ├── biz_suite_test.go
│   ├── user.go
│   └── user_test.go
├── go.mod
├── go.sum
├── main.go
└── mock
    └── mrepo
        └── user.go

3 directories, 7 files
  • 执行测试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
➜  p15 go test  p15/biz/ -v
=== RUN   TestBiz
Running Suite: biz user test
============================
Random Seed: 1657254031
Will run 4 of 4 specs


2022/07/08 12:20:31 /mnt/g/Workspace/go/docker/p15/biz/user.go:58 record not found
[0.800ms] [rows:0] SELECT * FROM `users` WHERE `users`.`phone` = '18655864568' ORDER BY `users`.`id` LIMIT 1

2022/07/08 12:20:31 /mnt/g/Workspace/go/docker/p15/biz/user.go:68 SLOW SQL >= 200ms
[264.312ms] [rows:1] INSERT INTO `users` (`phone`,`pwd`,`nickname`,`uname`,`id`) VALUES ('18655864568','admin123','hahaha','hahaha',4)
••••
Ran 4 of 4 Specs in 0.274 seconds
SUCCESS! -- 4 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestBiz (0.27s)
PASS
ok      p15/biz 0.286s

本文参考

coffee