go test

测试是代码流程中很重要的一个环节,但是课程内容上所说的测试过于简单,实际实习时候发现测试时候还是有很多点学校里都没怎么提到的,下面对于go的test做一个简短的总结。

Unit test

Construct UT with Ginkgo/Gomega

对想测试的文件的文件夹下,输入ginkgo bootstrap 生成 *_suite_test.go 文件,可以run直接启动测试。然后对于想要测试的文件 filenameA.go 用 ginkgo generate filenameA.go 生成测试用例代码。

Example

生成框架

对于这样一个 book.go 的代码文件,我们要对其进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package books

import (
"errors"
)

type Book struct {
Title string
Author string
Pages int
}

func (b *Book) CategoryByLength() (string, err.Error) {
if b.Pages < 0 {
err := errors.New("the number of pages is less than zero")
return nil, err.Error
}
if b.Pages >= 300 {
return "NOVEL", nil
}
return "SHORT STORY", nil
}

首先用 ginkgo bootstrap 生成 book_suite_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
package books_test

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

func TestBooks(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Books Suite")
}

TIPS:Go允许我们在books包同目录下指定books_test包。使用books_test而不是books可以让我们不破坏books包的封装:你的测试需要导入books包并从外部访问它,就像导入其它任何包一样。这是进入包,测试其内部结构并进行更多行为测试的首选。

• TestBooks是一个testing测试,运行go test或ginkgo时,Go测试运行器将运行此功能。

• Ginkgo 测试通过调用Fail(description string)功能来表示失败。我们使用RegisterFailHandler将此函数传递给Gomega。这是Ginkgo和Gomega之间的唯一连接点。

RunSpecs(t *testing.T, suiteDescription string)通知Ginkgo启动测试套件。任何specs失败,Ginkgo将自动使testing.T失败。

这个时候我们已经可以直接运行 go test 来进行测试流程了。

添加specs

对于 book.go 文件,我们用 ginkgo generate book.go 生成测试用例代码:

1
2
3
4
5
6
7
8
9
10
11
package books_test

import (
. "/path/to/books"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Book", func() {

})

• 我们使用Ginkgo的Describe(text string, body func()) bool函数添加了顶级Describe容器。

var _ = ...技巧允许我们在顶级评估Describe,而不必将其包装在func init() {}

接下来我们来编写自己所需要的 case :

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
var _ = Describe("Book", func() {
var (
longBook Book
shortBook Book
)

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

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

errorBook = Book{
Title: "Failure",
Author: "Dr. Mistake",
Pages: -1,
}
})

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

Context("With fewer than 300 pages", func() {
It("should be a short story", func() {
str, _ := shortBook.CategoryByLength()
Expect(str).To(Equal("SHORT STORY"))
})
})

Context("With fewer than 0 pages", func() {
It("should be a failed story", func() {
_, err := errorBook.CategoryByLength()
Expect(err).To(HaveOccurred())
})
})
})
})
  • BeforeEach是在多个测试用例中去除重复的步骤以及共享通用的设置。

  • Context 是为了测试同一请求的用例,例如,我们有一个处理程序来处理用户撤回请求。 可能是成功或失败,因此可以将两种情况放在相同的上下文中,它们是相同行为的结果,但情况不同。

  • It 是将同一测试用例的断言分开。 在上述示例中,在成功案例中,我们要验证结果,订单状态和资金帐户。 我们需要查询两个tables然后挨个检查字段,我们就可以编写两个It部分来分别验证这两个表。

ginkgo -r -coverpkg=$proj/service/... -cover -outputdir=$curPath -coverprofile=cov.out

(官方建议CI时候:ginkgo -r —randomizeAllSpecs —randomizeSuites —failOnPending —cover —trace —race —progress)

Mock

当我们想对某个函数进行测试时候,这个函数内部调用了一些RPC和http请求(比如DB),这时候我们假设这些外部请求自身的功能都是正确的,那么我们就可以将这些服务在测试函数中的接口mock掉,以免影响这些别的模块的服务正常运行。

TIPS:gomock配合ginkgo/gomega使用,只能用于测试,不建议在程序流程中使用。

mock步骤

  1. ​ 想清楚整体逻辑
  2. 定义想要(模拟)依赖项的 interface(接口)
  3. 使用 mockgen 命令对所需 mock 的 interface 生成 mock 文件
  4. 编写单元测试的逻辑,在测试中使用 mock
  5. 进行单元测试的验证

Example

对于这样一个目录树:

1
2
3
4
5
6
├── mock
├── person
│ └── male.go
└── user
├── user.go
└── user_test.go

person/male.go :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package person

var male MaleInf

type MaleInf interface {
Get(id int64) (int64, error)
}

type Male struct {
user string
}

func init() {
male = &Male{user: "caoxujie"}
}

func Set(inf MaleInf) {
male = inf
}

func (m *Male) Get (id int64) (int64, error) {
return id+1, nil
}

很多情况下,并不会有一个现成的interface供我们使用,我们需要创建一个包含我需要mock掉的函数的interface,然后用一个struct去实现这个interface(这里的实现方法是mock前的origin的方法),这样用到原先origin方法的地方都改成用接口变量的方法。此时正常线上会走init()方法,创建有origin方法的结构体并赋给接口变量。但mock的时候会走我们专门留的Set()方法,传一个写好了mock函数的struct进去赋给接口变量。这样再调用接口变量方法的时候,就会走我们新写的mock函数,大成功!

user/user.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package user

import "code.byted.org/caoxujie/mockdemo/person"

type User struct {
Person person.MaleInf
}

func NewUser(p person.MaleInf) *User {
return &User{Person: p}
}

func (u *User) GetUserInfo(id int64) (int64, error) {
result, err := u.Person.Get(id)
return result, err
}

对于这样的代码,我们可以在根目录执行mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock 来生成测试文件mock/male_mock.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
// Code generated by MockGen. DO NOT EDIT.
// Source: ./person/male.go

// Package mock is a generated GoMock package.
package mock

import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)

// MockMaleInf is a mock of MaleInf interface
type MockMaleInf struct {
ctrl *gomock.Controller
recorder *MockMaleInfMockRecorder
}

// MockMaleInfMockRecorder is the mock recorder for MockMaleInf
type MockMaleInfMockRecorder struct {
mock *MockMaleInf
}

// NewMockMaleInf creates a new mock instance
func NewMockMaleInf(ctrl *gomock.Controller) *MockMaleInf {
mock := &MockMaleInf{ctrl: ctrl}
mock.recorder = &MockMaleInfMockRecorder{mock}
return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockMaleInf) EXPECT() *MockMaleInfMockRecorder {
return m.recorder
}

// Get mocks base method
func (m *MockMaleInf) Get(id int64) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// Get indicates an expected call of Get
func (mr *MockMaleInfMockRecorder) Get(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMaleInf)(nil).Get), id)
}

之后我们就可以打开我们的test文件 user/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
package user

import (
"testing"
"code.byted.org/caoxujie/mockdemo/mock"
"github.com/golang/mock/gomock"
)

func TestUser_GetUserInfo(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()

var id int64 = 1
mockMale := mock.NewMockMaleInf(ctl)
gomock.InOrder(
mockMale.EXPECT().Get(id).Return(id+1, nil),
)

user := NewUser(mockMale)
res, err := user.GetUserInfo(id)
if err != nil {
t.Errorf("user.GetUserInfo err: %v", err)
}
println(id, res)
}

gomock提供的功能

参数匹配
1
2
3
4
5
mockObj.EXPECT().SomeMethod(gomock.Any(), "a").Return("4a") // x匹配任意值
mockObj.EXPECT().SomeMethod(gomock.Equal(4), "a").Return("4a") // x == 4
gomock.Len(length) // slice, array, map lenght
gomock.Nil()
gomock.Not()
调用次数限制
1
mockObj.EXPECT().SomeMethod(gomock.Any(),  "a").Return("4a") // 一次  mockObj.EXPECT().SomeMethod(gomock.Any(),  "a").Return("4a").AnyTimes() // 0或多次  mockObj.EXPECT().SomeMethod(gomock.Any(),  "a").Return("4a").Times(n) // n次  mockObj.EXPECT().SomeMethod(gomock.Any(),  "a").Return("4a").MaxTimes(n) //最多n次  mockObj.EXPECT().SomeMethod(gomock.Any(),  "a").Return("4a").MinTimes(n) //至少n次
调用顺序

默认情况下期待的函数调用没有顺序上的要求,需要时,通过After, InOrder限定顺序

After:

1
2
firstCall := mockObj.EXPECT().SomeMethod(1, "first")  
secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)

InOrder:

1
2
3
4
5
gomock.InOrder(    
mockObj.EXPECT().SomeMethod(1, "first"),
mockObj.EXPECT().SomeMethod(2, "second"),
mockObj.EXPECT().SomeMethod(3, "third"),
)
自定义内部流程

对于难以构造的输入(比如含rand, time相关的结构体)我们可以先用gomock.any()接受任意输入,然后自定义处理函数来判断到此层函数的输入是否符合我们的预期)

1
2
3
mockRuleDBWriteHandler.EXPECT().MCreateFactorRelations(gomock.Any(),  gomock.Any()).DoAndReturn(func(ctx context.Context ,  actualCreatefactorRelation []*dbmodel.FactorRelation) (int64, errs.Error) {
// ...
}).AnyTimes()

TIPS:用monky库来快速mock掉time.Now.UTC(),将所有的time.Now()输出都设置为定值:

1
2
3
monkey.Patch(time.Now, func() time.Time {    
return time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
})

有时甚至需要对http的req和resp进行mock,此时可以按原先方法在函数里构造response结构,也可以直接使用http_mock包对http传给transport层的接口和结构体进行mock(使用时不感知,但应该是全局mock我猜不能同时跑mock http和使用http的case,我也不太确定有没有并发问题)

Mock rpc interface

这边就不过多举例了,不然我怕容易被公司开orz

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
var _ = Describe("Rule", func() {

var (
mockCtrl *gomock.Controller
mockRuleDataInf *mock.MockRuleDataInf
)

BeforeEach(func() {
mockCtrl = gomock.NewController(GinkgoT())
defer mockCtrl.Finish()
mockRuleDataInf = mock.NewMockRuleDataInf(mockCtrl)
rpc.SetRuleDataInf(&mock.RuleDataRpcMock{})
})

Context("Successful", func() {
It("normal InsertRule case successfully", func() {
ctx := context.Background()
ctx = context.WithValue(ctx,"employee_key","caoxujie")

r := &front_model.CreateRulePara {
RuleName :"rule1",
Desc :"test rule",
RuleGroupId :"1",
Status :"2",
Priority :3,
ExpressionContent :"test",
DecisionList :[]front_model.Decision {{
DecisionType :"var_assignment",
RetType :"test",
VariableName :"test",
Value :"test",
FactorName :"test",
}},
NamespaceId :"111",

}
mockRuleDataInf.EXPECT().InsertRule(ctx, r).AnyTimes()

err := service.InsertRule(ctx, r)
Expect(err).Should(BeNil())
})
})
})