Ginkgo是针对Go程序进行BDD开发的工具,虽然它默认搭配使用gomega工具,不过我们还是建议你选择testify工具。
文档:
ps: 这个适合做e2e testing
demo
第一步:生成suite
$ cd path/to/books$ ginkgo bootstrap
会生成books_suite_test.go 文件
package books_testimport (. "github.com/onsi/ginkgo". "github.com/onsi/gomega""testing")func TestBooks(t *testing.T) {// ginkgo通过调用Fail(description string)函数来发出fail信号。// 我们用RegisterFailHandler() 将Fail函数传递给Gomega。// RegisterFailHandler()是连接ginkgo和gomega的唯一途径。RegisterFailHandler(Fail)RunSpecs(t, "Books Suite")}
因为我们不用gomega,而是用assert,手动改下该文件
package books_testimport (. "github.com/onsi/ginkgo""testing")func TestBooks(t *testing.T) {// 启动测试套件,如果任何一个specs失败,该套件则自动返回失败RunSpecs(t, "Books Suite")}
第二步:生成test
ginkgo generate
如果用gomega,可以看官方例子
package books_testimport (. "commons/books""github.com/stretchr/testify/assert". "github.com/onsi/ginkgo/v2")var _ = Describe("Book", func() {var (longBook BookshortBook Book)BeforeEach(func() {longBook = Book{Title: "Les Miserables",Author: "Victor Hugo",Pages: 2783,}shortBook = Book{Title: "Fox In Socks",Author: "Dr. Seuss",Pages: 24,}})Describe("Categorizing book length", func() {Context("With more than 300 pages", func() {It("should be a novel", func() {assert.Equal(GinkgoT(), longBook.CategoryByLength(), "NOVEL")})})Context("With fewer than 300 pages", func() {It("should be a short story", func() {assert.Equal(GinkgoT(), shortBook.CategoryByLength(), "SHORT STORY")})})})})
- 我们添加了一个顶层的描述容器,用Ginkgo的【Describe(text string, body func() ) bool 】函数。使用【var _ = …】允许我们直接描述填充顶层的Describe() 不需要用【func init(){}】初始化包装它。
- 【Describe() {}】中的函数包含了我们的使用规范。
- 为了共享BeforeEach和It之间的状态,Ginkgo使用闭包函数变量来构建顶层的Describe()和Context()
- Ginkgo中使用【Descirbe()】和【Context()】来描述代码的测试行为,将一个或多个测试用例It归类。
- Ginkgo中使用【BeforceEach()】来为specs设置状态,执行每个测试用例It前执行该段代码,使用【It()】来指定单个spec,是测试用例的基本单位,即It中包含的代码就算一个测试用例。
- 使用assert中的【Equal()】函数来设置期望。
第三步:执行用例
mac@weideMac-mini books % go test -v=== RUN TestBooksRunning Suite: Books Suite - /Users/mac/Desktop/go/commons/books================================================================Random Seed: 1641387640Will run 2 of 2 specs••Ran 2 of 2 Specs in 0.000 secondsSUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped--- PASS: TestBooks (0.00s)PASSok commons/books 0.011s
Writing Specs
ref:https://onsi.github.io/ginkgo/
------------------------------Given API POST /api/v1/xxx when yyyshould return bad requestSTEP: empty certificateSTEP: malformed certificate•------------------------------Given API POST /api/v1/controlplanes when yyyshould return bad request------------------------------
:::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 模块
var _ = Describe("Book", func() {It("can be loaded from JSON", func() {book := NewBookFromJSON(`{"title":"Les Miserables","author":"Victor Hugo","pages":1488}`)Expect(book.Title).To(Equal("Les Miserables"))Expect(book.Author).To(Equal("Victor Hugo"))Expect(book.Pages).To(Equal(1488))})})
BY
起提示作用,当测试失败时,可以很清楚的看到最后执行到哪了,成功的话是不会输出信息的。当然 加了 “-v” 参数,总是会输出
var _ = Describe("Browsing the library", func() {BeforeEach(func() {By("Fetching a token and logging in")authToken, err := authClient.GetToken("gopher", "literati")Expect(err).NotTo(HaveOccurred())Expect(libraryClient.Login(authToken)).To(Succeed())})It("should be a pleasant experience", func() {By("Entering an aisle")aisle, err := libraryClient.EnterAisle()Expect(err).NotTo(HaveOccurred())By("Browsing for books")books, err := aisle.GetBooks()Expect(err).NotTo(HaveOccurred())Expect(books).To(HaveLen(7))By("Finding a particular book")book, err := books.FindByTitle("Les Miserables")Expect(err).NotTo(HaveOccurred())Expect(book.Title).To(Equal("Les Miserables"))By("Checking a book out")Expect(libraryClient.CheckOut(book)).To(Succeed())books, err = aisle.GetBooks()Expect(err).NotTo(HaveOccurred())Expect(books).To(HaveLen(6))Expect(books).NotTo(ContainElement(book))})})
BeforeEach
使用BeforeEach() 共享模块的共用设置:
var _ = Describe("Book", func() {var book BookBeforeEach(func() {book = NewBookFromJSON(`{"title":"Les Miserables","author":"Victor Hugo","pages":1488}`)})It("can be loaded from JSON", func() {Expect(book.Title).To(Equal("Les Miserables"))Expect(book.Author).To(Equal("Victor Hugo"))Expect(book.Pages).To(Equal(1488))})It("can extract the author's last name", func() {Expect(book.AuthorLastName()).To(Equal("Hugo"))})})
1、 BeforeEach在每个Specs之前运行,从而确保每个Specs都有原始的State 副本。使用闭包的函数变量(在本例中为var book book)共享测试用例的初始状态。您还可以通过AfterEach模块执行清理。
2、 Tips:当有多层嵌套关系时, 外层的BeforeEach总是会被执行
package test_testimport ("fmt". "github.com/onsi/ginkgo/v2")var _ = Describe("Test", func() {BeforeEach(func(){fmt.Println("this is global beforeEach")})Context("test_c",func(){BeforeEach(func(){fmt.Println("this is Context beforeEach")})It("test_I",func(){fmt.Println("第一个IT")})})Context("test_c2",func(){It("test_I2",func(){fmt.Println("第二个IT")})})})this is global beforeEachthis is Context beforeEach第一个ITthis is global beforeEach第二个IT
通常我们还会在BeforeEach和AfterEach中加入断言。 这些断言可以判断在为测试用例准备State的时候有无发生错误。[
](https://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach)
Describe() & Context()
var _ = Describe("Book", func() {var (book Bookerr error)BeforeEach(func() {book, err = NewBookFromJSON(`{"title":"Les Miserables","author":"Victor Hugo","pages":1488}`)})Describe("loading from JSON", func() {Context("when the JSON fails to parse", func() {BeforeEach(func() {book, err = NewBookFromJSON(`{"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() {Expect(err).To(HaveOccurred())})})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())})})})Describe("Extracting the author's last name", func() {It("should correctly identify and return the last name", func() {Expect(book.AuthorLastName()).To(Equal("Hugo"))})})})
① 使用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来实现这一点。
package books_testimport (. "github.com/onsi/ginkgo". "github.com/onsi/gomega""your/db""testing")var dbRunner *db.Runnervar dbClient *db.Clientfunc 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()})
:::info
① BeforeSuite函数在所有Specs运行前执行,如果BeforeSuite报错,则后续测试套件不会执行。
② AfterSuite函数在所有Specs运行后执行,不论测试是否失败。因为AfterSuite通常包含清理现有状态的代码,所以当你向运行中的测试套件发送一个中断信号(^C)时,Ginkgo也会执行AfterSuite。要中止AfterSuite,需要发送另一个中断信号。
③ BeforeSuite和AfterSuite都只能创建一次。
④ 并行运行时,运行每个进程的前后都会执行一次BeforeSuite和AfterSuite。
:::
entry
entry 能规范好重复测试
package books_testimport (. "commons/books". "github.com/onsi/ginkgo/v2""github.com/stretchr/testify/assert")var _ = Describe("book", DescribeTable("Extracting the author's first and last name",func(author string, isValid bool, firstName string, lastName string) {book := &Book{Title: "My Book",Author: author,Pages: 10,}assert.Equal(GinkgoT(), book.IsValid(), isValid)assert.Equal(GinkgoT(), book.AuthorFirstName(), firstName)assert.Equal(GinkgoT(), book.AuthorLastName(), lastName)},Entry("When author has both names", "Victor Hugo", true, "Victor", "Hugo"),Entry("When author has one name", "Hugo", true, "", "Hugo"),Entry("When author has a middle name", "Victor Marie Hugo", true, "Victor", "Hugo"),Entry("When author has no name", "", false, "", ""),))
例如:上面的代码,可以写成下面这样
var _ = Describe("Extracting the author's first and last name", func() {It("When author has both names", func() {book := &Book{Title: "My Book",Author: "Victor Hugo",Pages: 10,}assert.Equal(GinkgoT(), book.IsValid(), true)assert.Equal(GinkgoT(), book.AuthorFirstName(), "Victor")assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")})It("When author has one name", func() {book := &Book{Title: "My Book",Author: "Hugo",Pages: 10,}assert.Equal(GinkgoT(), book.IsValid(), true)assert.Equal(GinkgoT(), book.AuthorFirstName(), "")assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")})It("When author has a middle name", func() {book := &Book{Title: "My Book",Author: "Victor Marie Hugo",Pages: 10,}assert.Equal(GinkgoT(), book.IsValid(), true)assert.Equal(GinkgoT(), book.AuthorFirstName(), "Victor")assert.Equal(GinkgoT(), book.AuthorLastName(), "Hugo")})It("When author has no name", func() {book := &Book{Title: "My Book",Author: "",Pages: 10,}assert.Equal(GinkgoT(), book.IsValid(), false)assert.Equal(GinkgoT(), book.AuthorFirstName(), "")assert.Equal(GinkgoT(), book.AuthorLastName(), "lastName")})})
很明显,实现上面的方式更加简洁
注意:BeforeEach在树型构建阶段构建的DescribeTable有时会让用户感到困惑。具体来说,容器节点中声明的变量在树构建阶段还没有初始化。正因如此,以下方法将无法工作:
Describe("book", func() {var shelf map[string]*books.Book //Shelf is declared hereBeforeEach(func() {shelf = map[string]*books.Book{ //...and initialized here"Les Miserables": &books.Book{Title: "Les Miserables", Author: "Victor Hugo", Pages: 2783},"Fox In Socks": &books.Book{Title: "Fox In Socks", Author: "Dr. Seuss", Pages: 24},}})DescribeTable("Categorizing books",func(book *books.Book, category books.Category) {Expect(book.Category()).To(Equal(category))},Entry("Novels", shelf["Les Miserables"], books.CategoryNovel),Entry("Novels", shelf["Fox in Socks"], books.CategoryShortStory),)})
这些spec将会失败。当在树构建阶段调用可描述的和条目时,shelf已声明但未初始化。因此shelf[“Les Miserables”]将返回一个nil指针,规范将失败。
为了解决这个问题,我们必须将对shelf变量的访问移到spec闭包的主体中
Describe("book", func() {var shelf map[string]*books.Book //Shelf is declared hereBeforeEach(func() {shelf = map[string]*books.Book{ //...and initialized here"Les Miserables": &books.Book{Title: "Les Miserables", Author: "Victor Hugo", Pages: 2783},"Fox In Socks": &books.Book{Title: "Fox In Socks", Author: "Dr. Seuss", Pages: 24},}})DescribeTable("Categorizing books",func(key string, category books.Category) {Expect(shelf[key]).To(Equal(category))},Entry("Novels", "Les Miserables", books.CategoryNovel),Entry("Novels", "Fox in Socks", books.CategoryShortStory),)})
focus
几乎所有的specs 都有focus方法,比如:FDescribe即只会运行这一个Describe
�
执行顺序
:::info
总结:
可以看出,Describe 并不是完全按顺序执行,只有specs 中的部分才是顺序执行
:::


