文章目录
简介
在如何有效地测试Go代码一文中,我们谈论了单元测试,针对它的两大难点:解耦、依赖,提出了面向接口、mock 依赖的解决方案。同时,该文还讨论了一些 Go 领域内的实用测试工具,欢迎读者阅读。单元测试关注点是代码逻辑单元,一般是一个对象或者一个具体函数。我们可以编写足够的单元测试来确保代码的质量,当功能修改或代码重构时,充分的单元测试案例能够给予我们足够的信心。单元测试之上是开发规范。在敏捷软件开发中,有两位常客:测试驱动开发(Test-Driven Development,TDD)和行为驱动开发(Behavior-driven development,BDD)。它们是实践与技术,同时也是设计方法论。
1. TDD
TDD 的基本思路就是通过测试来推动整个开发的进行,原则就是在开发功能代码之前,先编写单元测试用例。包含以下五个步骤:
- 开发者首先写一些测试用例,
- 运行这些测试,但这些测试明显都会失败,因为测试用例中的业务逻辑还没实现。
- 实现代码细节
- 如果开发者顺利实现代码的话,运行所有测试就会通过
- 对业务代码及时重构,如果新代码功能不正确的话,对应的测试文件也会失败
当需要开发新功能时,重复上述步骤。流程如下图所示
有一个 Github 仓库比较有趣:learn-go-with-tests,该仓库旨在通过 Go 学习 TDD 。
2. BDD
TDD 侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码。而BDD更加侧重设计,其要求在设计测试用例时对系统进行定义,倡导使用通用的语言将系统的行为描述出来,将系统设计和测试用例结合起来,以此为驱动进行开发工作。BDD 衍生于 TDD,主要区别就是在于测试的描述上。BDD 使用一种更通俗易懂的文字来描述测试用例,更关注需求的功能,而不是实际结果。BDD 赋予的像阅读句子一样阅读测试的能力带来对测试认知上的转变,有助于我们去考虑如何更好写测试。
3. Ginkgo
Ginkgo是一个 Go 语言的 BDD 测试框架,旨在帮助开发者编写富有表现力的全方位测试。Ginkgo 集成了 Go 原生的库,这意味着你可以通过来运行 Ginkgo 测试套件。同时,它与断言和 mock 套件testify、富测试集go-check同样兼容。但 Ginkgo 建议的是搭配gomega库一起使用。
Ginkgo10个常用的模块:It、Context、Describe、BeforeEach、AfterEach、JustBeforeEach、BeforeSuite、AfterSuite、By、Fail
- It是测试例的基本单位,即It包含的代码就算一个测试用例
- Context和Describe的功能都是将一个或多个测试例归类
- BeforeEach是每个测试例执行前执行该段代码
- AfterEach是每个测试例执行后执行该段代码
- JustBeforeEach是在BeforeEach执行之后,测试例执行之前执行
- BeforeSuite是在该测试集执行前执行,即该文件夹内的测试例执行之前
- AfterSuite是在该测试集执行后执行,即该文件夹内的测试例执行完后
- By是打印信息,内容只能是字符串,只会在测试例失败后打印,一般用于调试和定位问题
- Fail是标志该测试例运行结果为失败,并打印里面的信息
- 还有一个Specify和It功能完全一样,It属于其简写
一、Ginkgo实践
1. 安装Ginkgo
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...
2. 使用
创建一个测试文件夹如example,进入文件夹,执行命令ginkgo bootstrap生成模版文件,文件名是example_suite_test.go,里面有入口函数 执行ginkgo generate example,example可以不写,默认是当前文件夹名称,生成测试例模板文件example_test.go 加_test后缀是为了和当前文件夹内已有代码做区分 example_test.go代码中默认会import当前文件夹
var_=Describe(“Book”,func(){var(
book Book
err error
json string)BeforeEach(func(){
json =`{
"title":"Les Miserables",
"author":"Victor Hugo",
"pages":1488
}`})JustBeforeEach(func(){
book, err =NewBookFromJSON(json)})AfterEach(func(){By("End One Test")})Describe("loading from JSON",func(){Context("when the JSON parses succesfully",func(){It("should populate the fields correctly",func(){Expect(book.Title).To(Equal("Les Miserables"))Expect(book.Author).To(Equal("Victor Hugo"))Expect(book.Pages).To(Equal(1488))})It("should not error",func(){Expect(err).NotTo(HaveOccurred())})})Context("when the JSON fails to parse",func(){BeforeEach(func(){
json =`{
"title":"Les Miserables",
"author":"Victor Hugo",
"pages":1488oops
}`})It("should return the zero-value for the book",func(){Expect(book).To(BeZero())})It("should error",func(){if err !=nil{Fail("This Case Failed")}})})})Describe("Extracting the author's last name",func(){It("should correctly identify and return the last name",func(){Expect(book.AuthorLastName()).To(Equal("Hugo"))})})})
可以看到,首先定义了全局变量book、err和json。 五个测试例分成两大类,由两个Describe区分,第一类又分成两小类,用Context做区分。每个It包含的就是一个测试用例。 由两个BeforeEach,每个BeforeEach只在当前域内起作用。执行顺序是同一层级的顺序执行,不同层级的从外层到里层以此执行。AfterEach该规则相反。 AfterEach一般用于测试例执行完成后进行数据清理,也可以用于结果判断 尽量不要在var里面给变量赋值,因为每次执行测试用例都有可能改变全局变量的值,会对后面的测试例产生影响,写在BeforeEach中比较合适
funcTestBooks(t *testing.T){RegisterFailHandler(Fail)RunSpecs(t,"Books Suite")}var_=BeforeSuite(func(){
dbRunner = db.NewRunner()
err := dbRunner.Start()Expect(err).NotTo(HaveOccurred())
dbClient = db.NewClient()
err = dbClient.Connect(dbRunner.Address())Expect(err).NotTo(HaveOccurred())})var_=AfterSuite(func(){
dbClient.Cleanup()
dbRunner.Stop()})
BeforeSuite和AfterSuite写在_suite_test.go文件中,会在所有测试例执行之前和之后执行 如果BeforeSuite执行失败,则这个测试集都不会被执行
Tip:使用C中断执行时,AfterSuite仍然会被执行,需要再使用一次C中断
二、高级用法
1. 标志
有三个:F、X和P,可以用在Describe、Context、It等任何包含测试例的模块,F含义Focus,使用后表示只执行该模块包含的测试
FDescribe(“outer describe”,func(){It(“A”,func(){ … })It(“B”,func(){ … })})
Tip:当里层和外层都存在Focus时,外层的无效,即下面代码只会执行B测试用例
FDescribe(“outer describe”,func(){It(“A”,func(){ … })FIt(“B”,func(){ … })})
P的含义是Pending,即不执行,用法和F一样,规则的外层的生效X和P的含义一样 还有一个跳过测试例的方式是在代码中加
Skip It(“should do something,if it can”,func(){if!someCondition {Skip(“special condition wasn’t met”)}})
2. 并发
ginkgo -p 使用默认并发数,ginkgo -nodes=N 自己设定并发数,默认并发数是用的参数runtime.NumCPU()值,即逻辑CPU个数,大于4时,用runtime.NumCPU()-1,并发执行时打印的日志是汇总后经过合并处理再打印的,所以看起来比较规范,每个测试例的内容也都打印在一起,但时不实时,如果需要实时打印,加-stream参数,缺点是每个测试例日志交叉打印
3. goroutine
It(“should post to the channel, eventually”,func(done Done){
c :=make(chanstring,0)goDoSomething(c)Expect(<-c).To(ContainSubstring("Done!"))close(done)},0.2)
Ginkgo检测到Done类型参数,就会自动设置超时时间,就是后面那个0.2,单位是秒
4. DesctibeTable用法
有时候很多测试例除了数据部分其他都是相同的,写很多类似的It会很繁琐,于是有Table格式出现
package table_test
import(. “github.com/onsi/ginkgo/extensions/table”
."github.com/onsi/ginkgo"."github.com/onsi/gomega")var_=Describe(“Math”,func(){DescribeTable(“the > inequality”,func(x int, y int, expected bool){Expect(x > y).To(Equal(expected))},Entry(“x > y”,1,0,true),Entry(“x == y”,0,0,false),Entry(“x < y”,0,1,false),)})
等同于
package table_test import(. “github.com/onsi/ginkgo”
. “github.com/onsi/gomega”
)var_=Describe(“Math”,func(){Describe(“the > inequality”,It(“x > y”,func(){Expect(1>0).To(Equal(true))})It("x == y",func(){Expect(0>0).To(Equal(false))})It("x < y",func(){Expect(0>1).To(Equal(false))}))})
4. 生成JUnit测试报告
一般生成Junit的XML测试报告
funcTestFoo(t *testing.T){RegisterFailHandler(Fail)
junitReporter := reporters.NewJUnitReporter("junit.xml")RunSpecsWithDefaultAndCustomReporters(t,"Foo Suite",[]Reporter{junitReporter})}
6. 测试例性能
使用Measure模块
Measure(“it should do something hard efficiently”,func(b Benchmarker){
runtime := b.Time(“runtime”,func(){
output :=SomethingHard()Expect(output).To(Equal(17))})
Ω(runtime.Seconds()).Should(BeNumerically("<",0.2),"SomethingHard() shouldn't take too long.")
b.RecordValue("disk usage (in MB)",HowMuchDiskSpaceDidYouUse())},10)
该测试例会运行10次,并打印出执行性能数据
• [MEASUREMENT]
Suite
it should do something hard efficiently
Ran 10 samples:
runtime:
Fastest Time: 0.01s
Slowest Time: 0.08s
Average Time: 0.05s ± 0.02s
disk usage (in MB):
Smallest: 3.0
Largest: 5.2
Average: 3.9 ± 0.4
小结
DD 和 BDD 是敏捷开发中常被提到的方法论。与TDD相比,BDD 通过编写行为和规范来驱动软件开发。这些行为和规范在代码中体现于更”繁琐“的描述信息。关于 BDD 的本质,有另外一种表达方式:BDD 帮助开发人员设计软件,TDD 帮助开发人员测试软件。Ginkgo 是 Go 语言中非常优秀的 BDD 框架,它通过 DSL 语法(Describe/Context/It)有效地帮助开发者组织与编排测试用例。本文只是展示了 Ginkgo 非常简单的用例,权当是抛砖引玉。读者在使用 Ginkgo 过程中,需要理解它的执行生命周期, 重点包括 这些模块的执行顺序与语义逻辑。Ginkgo 有很多的功能本文并未涉及,例如异步测试、基准测试、持续集成等强大的支持。其仓库位于 https://github.com/onsi/ginkgo ,同时提供了英文版与中文版使用文档,读者可以借此了解更多 Ginkgo 信息。最后,K8s项目中也使用了 Ginkgo 框架,用于编写其端到端 (End to End,E2E) 测试用例,值得借鉴学习。
版权归原作者 Freedom3568 所有, 如有侵权,请联系我们删除。