Ginkgo是针对Go程序进行BDD开发的工具,虽然它默认搭配使用gomega工具,不过我们还是建议你选择testify工具。
文档:
ps: 这个适合做e2e testing
demo
第一步:生成suite
$ cd path/to/books
$ ginkgo bootstrap
会生成books_suite_test.go 文件
package books_test
import (
. "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_test
import (
. "github.com/onsi/ginkgo"
"testing"
)
func TestBooks(t *testing.T) {
// 启动测试套件,如果任何一个specs失败,该套件则自动返回失败
RunSpecs(t, "Books Suite")
}
第二步:生成test
ginkgo generate
如果用gomega,可以看官方例子
package books_test
import (
. "commons/books"
"github.com/stretchr/testify/assert"
. "github.com/onsi/ginkgo/v2"
)
var _ = Describe("Book", func() {
var (
longBook Book
shortBook 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 TestBooks
Running Suite: Books Suite - /Users/mac/Desktop/go/commons/books
================================================================
Random Seed: 1641387640
Will run 2 of 2 specs
••
Ran 2 of 2 Specs in 0.000 seconds
SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestBooks (0.00s)
PASS
ok commons/books 0.011s
Writing Specs
ref:https://onsi.github.io/ginkgo/
------------------------------
Given API POST /api/v1/xxx when yyy
should return bad request
STEP: empty certificate
STEP: malformed certificate
•
------------------------------
Given API POST /api/v1/controlplanes when yyy
should 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 Book
BeforeEach(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_test
import (
"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 beforeEach
this is Context beforeEach
第一个IT
this is global beforeEach
第二个IT
通常我们还会在BeforeEach和AfterEach中加入断言。 这些断言可以判断在为测试用例准备State的时候有无发生错误。[
](https://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach)
Describe() & Context()
var _ = Describe("Book", func() {
var (
book Book
err 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_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"your/db"
"testing"
)
var dbRunner *db.Runner
var dbClient *db.Client
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()
})
:::info
① BeforeSuite函数在所有Specs运行前执行,如果BeforeSuite报错,则后续测试套件不会执行。
② AfterSuite函数在所有Specs运行后执行,不论测试是否失败。因为AfterSuite通常包含清理现有状态的代码,所以当你向运行中的测试套件发送一个中断信号(^C)时,Ginkgo也会执行AfterSuite。要中止AfterSuite,需要发送另一个中断信号。
③ BeforeSuite和AfterSuite都只能创建一次。
④ 并行运行时,运行每个进程的前后都会执行一次BeforeSuite和AfterSuite。
:::
entry
entry 能规范好重复测试
package books_test
import (
. "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 here
BeforeEach(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 here
BeforeEach(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 中的部分才是顺序执行
:::