• 0
  • 0
分享

  简介

  在如何有效地测试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是在该测试集执行后执行,即该文件夹内的测试例执行完后

  · y是打印信息,内容只能是字符串,只会在测试例失败后打印,一般用于调试和定位问题

  · 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中比较合适。

  func TestBooks(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(chan string, 0)
  go DoSomething(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测试报告:

  func TestFoo(t *testing.T) { 
  RegisterFailHandler(Fail) 
  junitReporter := reporters.NewJUnitReporter("junit.xml")
  RunSpecsWithDefaultAndCustomReporters(t, "Foo Suite", []Reporter{junitReporter}) 
  }

  5. 测试例性能

  使用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    

来源:http://www.51testing.com/html/28/n-7795128.html

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          •   之前,也和大家介绍过一些 redis 性能监控指标:Redis性能指标监控!,那么性能测试也是针对这些指标进行的。  Redis 包含一个名为 redis-benchmark 的性能测试工具,它可以模拟 N 个客户端同时向 Redis 发送 M 条查询命令的应用场景(类似于 Apache 的 ab 工具)。  语法格式  Redis 性能测试的基本命令如下:  注意:该命令是在 redis 的目录下执行的,而不是 redis 客户端的内部指令。  注意:redis-benchmark 的测试结果和硬件关联比较大,尤其是 CPU 主频的频率。  # 工具使用格式...
            0 0 637
            分享
          • 当项目已经上线并且趋于稳定,测试人员就不会每天对版本做一轮又一轮的回归测试。这时,开发在迭代版本中改动一点代码,没有告知测试人员,那么测试人员很难发现由改动引发的BUG。   这种情况下,测试人员确实无奈:产品也好、开发也罢,不主动告知改动内容,测试人员会把它默认成为上一个稳定版本,不会整天放精力在这类版本的回归测试中。再者,测试人员每天都对所有项目做手工的回归测试也不现实。这造成测试人员很被动。测试人员若想提高产品质量只能寄希望于高效的团队管理,加上与产品、开发人员的及时沟通、提醒。   测试人员能不能改变这种被动的局面,全权掌握版本的...
            5 2 2994
            分享
          •   社交媒体 X 周二晚些时候表示,它已经完成了对美国证券交易委员会(U.S. Securities and Exchange Commission)账户被入侵的初步调查,该账户被黑后展示了一个精心设计的虚假帖文,声称美国证券交易委员会已经批准比特币 ETF 进行交易。  "根据我们的调查,此次泄露并非由于X的系统遭到任何破坏,而是由于一个身份不明的人通过第三方获得了与@SECGov账户相关的电话号码的控制权,"X在帖子中确认美国证券交易委员会的账户遭到了泄露。  X 在帖子中说:"我们还可以确认,在账户被泄露时,该账户并未启用双因素身份验证。"  未经...
            0 0 700
            分享
          • 泸定6.8级地震发生时,四川、重庆部分居民的手机中都提前接到了地震预警信息,其中一张来自重庆网友的预警截图刷屏各大网站,“64秒后地震横波到达重庆市江北区,震中四川泸定,预警震级6.9级,震感较强”。一时间,“你手机的地震预警功能开了吗”“仅需10秒教大家如何打开手机地震预警”等话题迅速充上热搜。什么是地震预警?手机如何实现地震预警功能?9月7日,四川大学教授、地震预警与多灾种预警应用信息技术四川省重点实验室主任、成都高新减灾研究所所长王暾进行了解读。“预警”非“预报”多款国产手机已内置该功能“地震预警常与地震预报混淆,一字之差却谬以千里。”王暾说,地震预报是在地震还没有发生时,通过研究分析,...
            0 0 1055
            分享
          •   据熟悉内情的人士透露,谷歌母公司 Alphabet正在就以约 230 亿美元的价格收购网络安全初创公司 Wiz 进行深入谈判,这将是谷歌有史以来规模最大的一次收购。此外,它也是以色列公司最大的一笔收购,超过了目前由 Mobileye 保持的纪录,后者在 2017 年以 150 亿美元的价格出售给了英特尔。  这些人士说,如果谈判不破裂,协议可能很快就会达成。  在搜索公司和其他科技巨头受到严格的反垄断审查之际,Alphabet 正对这项交易虎视眈眈。这项收购还有助于推动Alphabet 在云计算领域的发展,云计算是一项重要且不断增长的业务,但 Alphabet 在这一领域一直落后于同行。 ...
            0 0 477
            分享
      • 51testing软件测试圈微信