Ginkgo是针对Go程序进行BDD开发的工具,虽然它默认搭配使用gomega工具,不过我们还是建议你选择testify工具。
文档:

ps: 这个适合做e2e testing

demo

第一步:生成suite

  1. $ cd path/to/books
  2. $ ginkgo bootstrap

会生成books_suite_test.go 文件

  1. package books_test
  2. import (
  3. . "github.com/onsi/ginkgo"
  4. . "github.com/onsi/gomega"
  5. "testing"
  6. )
  7. func TestBooks(t *testing.T) {
  8. // ginkgo通过调用Fail(description string)函数来发出fail信号。
  9. // 我们用RegisterFailHandler() 将Fail函数传递给Gomega。
  10. // RegisterFailHandler()是连接ginkgo和gomega的唯一途径。
  11. RegisterFailHandler(Fail)
  12. RunSpecs(t, "Books Suite")
  13. }

因为我们不用gomega,而是用assert,手动改下该文件

  1. package books_test
  2. import (
  3. . "github.com/onsi/ginkgo"
  4. "testing"
  5. )
  6. func TestBooks(t *testing.T) {
  7. // 启动测试套件,如果任何一个specs失败,该套件则自动返回失败
  8. RunSpecs(t, "Books Suite")
  9. }

第二步:生成test

  1. ginkgo generate

如果用gomega,可以看官方例子

  1. package books_test
  2. import (
  3. . "commons/books"
  4. "github.com/stretchr/testify/assert"
  5. . "github.com/onsi/ginkgo/v2"
  6. )
  7. var _ = Describe("Book", func() {
  8. var (
  9. longBook Book
  10. shortBook Book
  11. )
  12. BeforeEach(func() {
  13. longBook = Book{
  14. Title: "Les Miserables",
  15. Author: "Victor Hugo",
  16. Pages: 2783,
  17. }
  18. shortBook = Book{
  19. Title: "Fox In Socks",
  20. Author: "Dr. Seuss",
  21. Pages: 24,
  22. }
  23. })
  24. Describe("Categorizing book length", func() {
  25. Context("With more than 300 pages", func() {
  26. It("should be a novel", func() {
  27. assert.Equal(GinkgoT(), longBook.CategoryByLength(), "NOVEL")
  28. })
  29. })
  30. Context("With fewer than 300 pages", func() {
  31. It("should be a short story", func() {
  32. assert.Equal(GinkgoT(), shortBook.CategoryByLength(), "SHORT STORY")
  33. })
  34. })
  35. })
  36. })
  1. 我们添加了一个顶层的描述容器,用Ginkgo的【Describe(text string, body func() ) bool 】函数。使用【var _ = …】允许我们直接描述填充顶层的Describe() 不需要用【func init(){}】初始化包装它。
  2. 【Describe() {}】中的函数包含了我们的使用规范。
  3. 为了共享BeforeEach和It之间的状态,Ginkgo使用闭包函数变量来构建顶层的Describe()和Context()
  4. Ginkgo中使用【Descirbe()】和【Context()】来描述代码的测试行为,将一个或多个测试用例It归类。
  5. Ginkgo中使用【BeforceEach()】来为specs设置状态,执行每个测试用例It前执行该段代码,使用【It()】来指定单个spec,是测试用例的基本单位,即It中包含的代码就算一个测试用例。
  6. 使用assert中的【Equal()】函数来设置期望。

第三步:执行用例

  1. mac@weideMac-mini books % go test -v
  2. === RUN TestBooks
  3. Running Suite: Books Suite - /Users/mac/Desktop/go/commons/books
  4. ================================================================
  5. Random Seed: 1641387640
  6. Will run 2 of 2 specs
  7. ••
  8. Ran 2 of 2 Specs in 0.000 seconds
  9. SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped
  10. --- PASS: TestBooks (0.00s)
  11. PASS
  12. ok commons/books 0.011s

Writing Specs

ref:https://onsi.github.io/ginkgo/

  1. ------------------------------
  2. Given API POST /api/v1/xxx when yyy
  3. should return bad request
  4. STEP: empty certificate
  5. STEP: malformed certificate
  6. ------------------------------
  7. Given API POST /api/v1/controlplanes when yyy
  8. should return bad request
  9. ------------------------------

:::info

  • 不同的Describe由”—————-“ 隔开
  • Given API POST /api/v1/xxx 为Describe中的提示
  • when yyy 为Describe下context提示
  • should return bad request 为IT中的提示
  • STEP 为BY中的提示 :::

IT | Specify

Specify 是 IT 的别名,两者没用区别
可以向Describe() 或 Context() 中添加It 模块

  1. var _ = Describe("Book", func() {
  2. It("can be loaded from JSON", func() {
  3. book := NewBookFromJSON(`{
  4. "title":"Les Miserables",
  5. "author":"Victor Hugo",
  6. "pages":1488
  7. }`)
  8. Expect(book.Title).To(Equal("Les Miserables"))
  9. Expect(book.Author).To(Equal("Victor Hugo"))
  10. Expect(book.Pages).To(Equal(1488))
  11. })
  12. })

BY

起提示作用,当测试失败时,可以很清楚的看到最后执行到哪了,成功的话是不会输出信息的。当然 加了 “-v” 参数,总是会输出

  1. var _ = Describe("Browsing the library", func() {
  2. BeforeEach(func() {
  3. By("Fetching a token and logging in")
  4. authToken, err := authClient.GetToken("gopher", "literati")
  5. Expect(err).NotTo(HaveOccurred())
  6. Expect(libraryClient.Login(authToken)).To(Succeed())
  7. })
  8. It("should be a pleasant experience", func() {
  9. By("Entering an aisle")
  10. aisle, err := libraryClient.EnterAisle()
  11. Expect(err).NotTo(HaveOccurred())
  12. By("Browsing for books")
  13. books, err := aisle.GetBooks()
  14. Expect(err).NotTo(HaveOccurred())
  15. Expect(books).To(HaveLen(7))
  16. By("Finding a particular book")
  17. book, err := books.FindByTitle("Les Miserables")
  18. Expect(err).NotTo(HaveOccurred())
  19. Expect(book.Title).To(Equal("Les Miserables"))
  20. By("Checking a book out")
  21. Expect(libraryClient.CheckOut(book)).To(Succeed())
  22. books, err = aisle.GetBooks()
  23. Expect(err).NotTo(HaveOccurred())
  24. Expect(books).To(HaveLen(6))
  25. Expect(books).NotTo(ContainElement(book))
  26. })
  27. })

image.png

BeforeEach

使用BeforeEach() 共享模块的共用设置:

  1. var _ = Describe("Book", func() {
  2. var book Book
  3. BeforeEach(func() {
  4. book = NewBookFromJSON(`{
  5. "title":"Les Miserables",
  6. "author":"Victor Hugo",
  7. "pages":1488
  8. }`)
  9. })
  10. It("can be loaded from JSON", func() {
  11. Expect(book.Title).To(Equal("Les Miserables"))
  12. Expect(book.Author).To(Equal("Victor Hugo"))
  13. Expect(book.Pages).To(Equal(1488))
  14. })
  15. It("can extract the author's last name", func() {
  16. Expect(book.AuthorLastName()).To(Equal("Hugo"))
  17. })
  18. })

1、 BeforeEach在每个Specs之前运行,从而确保每个Specs都有原始的State 副本。使用闭包的函数变量(在本例中为var book book)共享测试用例的初始状态。您还可以通过AfterEach模块执行清理。
2、 Tips:当有多层嵌套关系时, 外层的BeforeEach总是会被执行

  1. package test_test
  2. import (
  3. "fmt"
  4. . "github.com/onsi/ginkgo/v2"
  5. )
  6. var _ = Describe("Test", func() {
  7. BeforeEach(func(){
  8. fmt.Println("this is global beforeEach")
  9. })
  10. Context("test_c",func(){
  11. BeforeEach(func(){
  12. fmt.Println("this is Context beforeEach")
  13. })
  14. It("test_I",func(){
  15. fmt.Println("第一个IT")
  16. })
  17. })
  18. Context("test_c2",func(){
  19. It("test_I2",func(){
  20. fmt.Println("第二个IT")
  21. })
  22. })
  23. })
  24. this is global beforeEach
  25. this is Context beforeEach
  26. 第一个IT
  27. this is global beforeEach
  28. 第二个IT

通常我们还会在BeforeEach和AfterEach中加入断言。 这些断言可以判断在为测试用例准备State的时候有无发生错误。[

](https://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach)

Describe() & Context()

  1. var _ = Describe("Book", func() {
  2. var (
  3. book Book
  4. err error
  5. )
  6. BeforeEach(func() {
  7. book, err = NewBookFromJSON(`{
  8. "title":"Les Miserables",
  9. "author":"Victor Hugo",
  10. "pages":1488
  11. }`)
  12. })
  13. Describe("loading from JSON", func() {
  14. Context("when the JSON fails to parse", func() {
  15. BeforeEach(func() {
  16. book, err = NewBookFromJSON(`{
  17. "title":"Les Miserables",
  18. "author":"Victor Hugo",
  19. "pages":1488oops
  20. }`)
  21. })
  22. It("should return the zero-value for the book", func() {
  23. Expect(book).To(BeZero())
  24. })
  25. It("should error", func() {
  26. Expect(err).To(HaveOccurred())
  27. })
  28. })
  29. Context("when the JSON parses succesfully", func() {
  30. It("should populate the fields correctly", func() {
  31. Expect(book.Title).To(Equal("Les Miserables"))
  32. Expect(book.Author).To(Equal("Victor Hugo"))
  33. Expect(book.Pages).To(Equal(1488))
  34. })
  35. It("should not error", func() {
  36. Expect(err).NotTo(HaveOccurred())
  37. })
  38. })
  39. })
  40. Describe("Extracting the author's last name", func() {
  41. It("should correctly identify and return the last name", func() {
  42. Expect(book.AuthorLastName()).To(Equal("Hugo"))
  43. })
  44. })
  45. })

使用Describe()来描述这段代码的行为,使用Context()来描述表达该行为在不同的环境下执行。在例子中,第一个测试行为Describe()是要导入json,测试用例有两种情景,一种是导入成功,一种是导入失败。
当Describe/Context 嵌套BeforeEach和AfterEach时,Describe/Context下每一个It模块,都会执行一次BeforeEach和AfterEach,以确保每个Specs都处于原始状态。

JustBeforeEach

JustBeforeEach() 模块在所有BeforeEach模块执行之后,It模块执行之前运行。所以,可以在BeforeEach()中创建配置参数,在JustBeforeEach()中进行导入和创建。

这样可以将It 执行前的条件配置关系更加多元化,减少重复代码。

BeforeSuite & AfterSuite

有时您希望在执行整个测试套件之前运行一些代码,或者完成整个测试套件之后运行一些代码。例如,可能需要启动和关闭外部数据库。

Ginkgo提供了BeforeSuite 和AfterSuite来实现这一点。

  1. package books_test
  2. import (
  3. . "github.com/onsi/ginkgo"
  4. . "github.com/onsi/gomega"
  5. "your/db"
  6. "testing"
  7. )
  8. var dbRunner *db.Runner
  9. var dbClient *db.Client
  10. func TestBooks(t *testing.T) {
  11. RegisterFailHandler(Fail)
  12. RunSpecs(t, "Books Suite")
  13. }
  14. var _ = BeforeSuite(func() {
  15. dbRunner = db.NewRunner()
  16. err := dbRunner.Start()
  17. Expect(err).NotTo(HaveOccurred())
  18. dbClient = db.NewClient()
  19. err = dbClient.Connect(dbRunner.Address())
  20. Expect(err).NotTo(HaveOccurred())
  21. })
  22. var _ = AfterSuite(func() {
  23. dbClient.Cleanup()
  24. dbRunner.Stop()
  25. })

:::info ① BeforeSuite函数在所有Specs运行前执行,如果BeforeSuite报错,则后续测试套件不会执行。
② AfterSuite函数在所有Specs运行后执行,不论测试是否失败。因为AfterSuite通常包含清理现有状态的代码,所以当你向运行中的测试套件发送一个中断信号(^C)时,Ginkgo也会执行AfterSuite。要中止AfterSuite,需要发送另一个中断信号。
③ BeforeSuite和AfterSuite都只能创建一次。
④ 并行运行时,运行每个进程的前后都会执行一次BeforeSuite和AfterSuite。 :::

entry

entry 能规范好重复测试

  1. package books_test
  2. import (
  3. . "commons/books"
  4. . "github.com/onsi/ginkgo/v2"
  5. "github.com/stretchr/testify/assert"
  6. )
  7. var _ = Describe("book", DescribeTable("Extracting the author's first and last name",
  8. func(author string, isValid bool, firstName string, lastName string) {
  9. book := &Book{
  10. Title: "My Book",
  11. Author: author,
  12. Pages: 10,
  13. }
  14. assert.Equal(GinkgoT(), book.IsValid(), isValid)
  15. assert.Equal(GinkgoT(), book.AuthorFirstName(), firstName)
  16. assert.Equal(GinkgoT(), book.AuthorLastName(), lastName)
  17. },
  18. Entry("When author has both names", "Victor Hugo", true, "Victor", "Hugo"),
  19. Entry("When author has one name", "Hugo", true, "", "Hugo"),
  20. Entry("When author has a middle name", "Victor Marie Hugo", true, "Victor", "Hugo"),
  21. Entry("When author has no name", "", false, "", ""),
  22. ))

例如:上面的代码,可以写成下面这样

  1. var _ = Describe("Extracting the author's first and last name", func() {
  2. It("When author has both names", func() {
  3. book := &Book{
  4. Title: "My Book",
  5. Author: "Victor Hugo",
  6. Pages: 10,
  7. }
  8. assert.Equal(GinkgoT(), book.IsValid(), true)
  9. assert.Equal(GinkgoT(), book.AuthorFirstName(), "Victor")
  10. assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")
  11. })
  12. It("When author has one name", func() {
  13. book := &Book{
  14. Title: "My Book",
  15. Author: "Hugo",
  16. Pages: 10,
  17. }
  18. assert.Equal(GinkgoT(), book.IsValid(), true)
  19. assert.Equal(GinkgoT(), book.AuthorFirstName(), "")
  20. assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")
  21. })
  22. It("When author has a middle name", func() {
  23. book := &Book{
  24. Title: "My Book",
  25. Author: "Victor Marie Hugo",
  26. Pages: 10,
  27. }
  28. assert.Equal(GinkgoT(), book.IsValid(), true)
  29. assert.Equal(GinkgoT(), book.AuthorFirstName(), "Victor")
  30. assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")
  31. })
  32. It("When author has no name", func() {
  33. book := &Book{
  34. Title: "My Book",
  35. Author: "",
  36. Pages: 10,
  37. }
  38. assert.Equal(GinkgoT(), book.IsValid(), false)
  39. assert.Equal(GinkgoT(), book.AuthorFirstName(), "")
  40. assert.Equal(GinkgoT(), book.AuthorLastName(), "lastName")
  41. })
  42. })

很明显,实现上面的方式更加简洁

注意:BeforeEach在树型构建阶段构建的DescribeTable有时会让用户感到困惑。具体来说,容器节点中声明的变量在树构建阶段还没有初始化。正因如此,以下方法将无法工作:

  1. Describe("book", func() {
  2. var shelf map[string]*books.Book //Shelf is declared here
  3. BeforeEach(func() {
  4. shelf = map[string]*books.Book{ //...and initialized here
  5. "Les Miserables": &books.Book{Title: "Les Miserables", Author: "Victor Hugo", Pages: 2783},
  6. "Fox In Socks": &books.Book{Title: "Fox In Socks", Author: "Dr. Seuss", Pages: 24},
  7. }
  8. })
  9. DescribeTable("Categorizing books",
  10. func(book *books.Book, category books.Category) {
  11. Expect(book.Category()).To(Equal(category))
  12. },
  13. Entry("Novels", shelf["Les Miserables"], books.CategoryNovel),
  14. Entry("Novels", shelf["Fox in Socks"], books.CategoryShortStory),
  15. )
  16. })

这些spec将会失败。当在树构建阶段调用可描述的和条目时,shelf已声明但未初始化。因此shelf[“Les Miserables”]将返回一个nil指针,规范将失败。

为了解决这个问题,我们必须将对shelf变量的访问移到spec闭包的主体中

  1. Describe("book", func() {
  2. var shelf map[string]*books.Book //Shelf is declared here
  3. BeforeEach(func() {
  4. shelf = map[string]*books.Book{ //...and initialized here
  5. "Les Miserables": &books.Book{Title: "Les Miserables", Author: "Victor Hugo", Pages: 2783},
  6. "Fox In Socks": &books.Book{Title: "Fox In Socks", Author: "Dr. Seuss", Pages: 24},
  7. }
  8. })
  9. DescribeTable("Categorizing books",
  10. func(key string, category books.Category) {
  11. Expect(shelf[key]).To(Equal(category))
  12. },
  13. Entry("Novels", "Les Miserables", books.CategoryNovel),
  14. Entry("Novels", "Fox in Socks", books.CategoryShortStory),
  15. )
  16. })

focus

几乎所有的specs 都有focus方法,比如:FDescribe即只会运行这一个Describe

执行顺序

:::info 总结:
可以看出,Describe 并不是完全按顺序执行,只有specs 中的部分才是顺序执行 ::: image.png