测试是代码流程中很重要的一个环节,但是课程内容上所说的测试过于简单,实际实习时候发现测试时候还是有很多点学校里都没怎么提到的,下面对于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 | package books |
首先用 ginkgo bootstrap 生成 book_suite_test.go 文件:
1 | package books_test |
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 | package books_test |
• 我们使用Ginkgo的Describe(text string, body func()) bool
函数添加了顶级Describe容器。
• var _ = ...
技巧允许我们在顶级评估Describe,而不必将其包装在func init() {}
接下来我们来编写自己所需要的 case :
1 | var _ = Describe("Book", func() { |
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步骤
- 想清楚整体逻辑
- 定义想要(模拟)依赖项的 interface(接口)
- 使用 mockgen 命令对所需 mock 的 interface 生成 mock 文件
- 编写单元测试的逻辑,在测试中使用 mock
- 进行单元测试的验证
Example
对于这样一个目录树:
1 | ├── mock |
person/male.go :
1 | package person |
很多情况下,并不会有一个现成的interface供我们使用,我们需要创建一个包含我需要mock掉的函数的interface,然后用一个struct去实现这个interface(这里的实现方法是mock前的origin的方法),这样用到原先origin方法的地方都改成用接口变量的方法。此时正常线上会走init()
方法,创建有origin方法的结构体并赋给接口变量。但mock的时候会走我们专门留的Set()方法,传一个写好了mock函数的struct进去赋给接口变量。这样再调用接口变量方法的时候,就会走我们新写的mock函数,大成功!
user/user.go:
1 | package user |
对于这样的代码,我们可以在根目录执行mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock
来生成测试文件mock/male_mock.go
:
1 | // Code generated by MockGen. DO NOT EDIT. |
之后我们就可以打开我们的test文件 user/user_test.go :
1 | package user |
gomock提供的功能
参数匹配
1 | mockObj.EXPECT().SomeMethod(gomock.Any(), "a").Return("4a") // x匹配任意值 |
调用次数限制
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 | firstCall := mockObj.EXPECT().SomeMethod(1, "first") |
InOrder:
1 | gomock.InOrder( |
自定义内部流程
对于难以构造的输入(比如含rand, time相关的结构体)我们可以先用gomock.any()接受任意输入,然后自定义处理函数来判断到此层函数的输入是否符合我们的预期)
1 | mockRuleDBWriteHandler.EXPECT().MCreateFactorRelations(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context , actualCreatefactorRelation []*dbmodel.FactorRelation) (int64, errs.Error) { |
TIPS:用monky库来快速mock掉time.Now.UTC(),将所有的time.Now()输出都设置为定值:
1 | monkey.Patch(time.Now, func() time.Time { |
有时甚至需要对http的req和resp进行mock,此时可以按原先方法在函数里构造response结构,也可以直接使用http_mock包对http传给transport层的接口和结构体进行mock(使用时不感知,但应该是全局mock我猜不能同时跑mock http和使用http的case,我也不太确定有没有并发问题)
Mock rpc interface
这边就不过多举例了,不然我怕容易被公司开orz
1 | var _ = Describe("Rule", func() { |