Ginkgo /ˈɡɪŋkoʊ /
是Go语言的一个行为驱动开发(BDD, Behavior-Driven Development)风格的测试框架,通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。Ginkgo 集成了Go语言的测试机制,你可以通过 go test 来运行Ginkgo测试套件。本文所有的代码可以在我的 Github 中找到。
技术特点 Behavior Driven Development Ginkgo最大的特点就是对BDD风格的支持。比如:
1 2 3 Describe("delete app api" , func () { It("should delete app permanently" , func () {...}) It("should delete app failed if services existed" , func () {...})
Ginkgo 定义的 DSL 语法(Describe/Context/It)可以非常方便的帮助大家组织和编排测试用例。在BDD模式中,测试用例的标题书写,要非常注意表达,要能清晰的指明用例测试的业务场景。只有这样才能极大的增强用例的可读性,降低使用和维护的心智负担。
可读性这一点,在自动化测试用例设计原则上,非常重要。因为测试用例不同于一般意义上的程序,它在绝大部分场景下,看起来都像是一段段独立的方法,每个方法背后隐藏的业务逻辑也是细小的,不具通识性。这个问题在用例量少的情况下,还不明显。但当用例数量上到一定量级,你会发现,如果能快速理解用例到底是能做什么的,真的非常重要。而这正是 BDD 能补足的地方。
不过还是要强调,Ginkgo 只是提供对 BDD 模式的支持,你的用例最终呈现的效果,还是依赖你自己的书写。
进程级并行,稳定高效 相应的我们知道,BDD 框架,因为其DSL的深度嵌套支持,会存在一些共享上下文的资源,如此的话想做线程级的并发会比较困难。而Ginkgo巧妙的避开了这个问题,它通过在运行时,运行多个被测服务的进程,来达到真正的并行,稳定性大大提高。其使用姿势也非常简单,ginkgo -p
命令就可以。在实践中,我们通常使用32核以上的服务器来跑集测,执行效率非常高。
这里有个细节,Ginkgo 虽然并行执行测试用例,但其输出的日志和测试报告格式,仍然是整齐不错乱的,这是如何做到的呢?原来,通过源码会发现,ginkgo CLI 工具在并行跑用例时,其内部会起一个监听随机端口的本地服务器,来做不同进程之间的消息同步,以及日志和报告的聚合工作,是不是很巧妙?
使用实战 安装 1 $ go get -u github.com/onsi/ginkgo/ginkgo
起步 创建套件 假设我有一个 books
的包,当前非常简单,有函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package bookstype Book struct { Title string Author string Pages int } func (b *Book) CategoryByLength () string { if b.Pages >= 300 { return "NOVEL" } return "SHORT STORY" }
假设我们想给 books
包编写 Ginkgo 测试,则首先需要使用命令创建一个Ginkgo test suite:
1 2 $ cd pkg/books $ ginkgo bootstrap
上述命令会生成文件:
/pkg/books/books_suite_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package books_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) func TestBooks (t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Books Suite" ) }
现在,使用命令 ginkgo或者 go test即可执行测试套件。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ginkgo Running Suite: Books Suite ========================== Random Seed: 1618566161 Will run 0 of 0 specs Ran 0 of 0 Specs in 0.000 seconds SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped PASS Ginkgo ran 1 suite in 3.113619498s Test Suite Passed
添加Spec 上面的空测试套件没有什么价值,我们需要在此套接下编写测试(Spec)。虽然可以在 books_suite_test.go
中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。
执行命令 ginkgo generate book
可以为源文件 books.go
生成测试:
pkg/books/book_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package books_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/SimpCosm/godemo/ginkgo/books" ) var _ = Describe("Book" , func () { })
我们可以添加一些Specs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package books_testimport ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/SimpCosm/godemo/ginkgo/books" ) var _ = Describe("Books" , func () { var ( longBook books.Book shortBook books.Book ) BeforeEach(func () { longBook = books.Book{ Title: "Les Miserables" , Author: "Victor Hugo" , Pages: 1488 , } shortBook = books.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 () { Expect(longBook.CategoryByLength()).To(Equal("NOVEL" )) }) }) Context("With fewer than 300 pages" , func () { It("should be a short story" , func () { Expect(shortBook.CategoryByLength()).To(Equal("SHORT STORY" )) }) }) }) })
运行上述测试显示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ginkgo Running Suite: Books Suite ========================== Random Seed: 1618571780 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 Ginkgo ran 1 suite in 1.8345466s Test Suite Passed
断言失败 除了调用Gomega之外,你还可以调用Fail函数直接断言失败:
Fail会记录当前进行的测试,并且触发panic,当前Spec的后续断言不会再进行。
通常情况下Ginkgo会 从panic中恢复,并继续下一个测试 。但是,如果你启动了一个Goroutine,并在其中触发了断言失败,则不会自动恢复,必须手工调用GinkgoRecover
:
1 2 3 4 5 6 7 8 9 10 11 It("panics in a goroutine" , func (done Done) { go func () { defer GinkgoRecover() Ω(doSomething()).Should(BeTrue()) close (done) }() })
记录日志 Ginkgo提供了一个全局可用的io.Writer
,名为GinkgoWriter
,供你写入。GinkgoWriter
在测试运行时聚合输入,并且只有在测试失败时才将其转储到stdout
。当以详细模式运行时(ginkgo -v
或go test -ginkgo.v
),GinkgoWriter
会立即将其输入重定向到stdout
。
当Ginkgo测试套件中断(通过^ C)时,Ginkgo将发出写入GinkgoWriter
的任何内容。这样可以更轻松地调试卡住的测试。 当与--progress
配对使用时将会特别有用,它指示Ginkgo在运行您的BeforeEaches
,Its
,AfterEaches
等时向GinkgoWriter发出通知。
传递参数 直接使用flag包即可:
1 2 3 4 var myFlag string func init () { flag.StringVar(&myFlag, "myFlag" , "defaultvalue" , "myFlag is used to control my behavior" ) }
执行测试时使用 ginkgo -- --myFlag=xxx
传递参数。
测试结构 单个Specs: It 你可以在Describe、Context这两种容器块内编写Spec,每个Spec写在It块中。
您可以通过在Describe
或Context
容器块中设置 It 块来添加单个 spec:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 )) }) })
It
也可以放在顶层,虽然这种情况并不常见。
Specify 别名 为了确保您的 specs 阅读自然,Specify
,PSpecify
,XSpecify
和FSpecify
块可用作别名,以便在相应的It
替代品看起来不像自然语言的情况下使用。
Specify
块的行为与It
块相同,可以在It
块(以及PIt
,XIt
和FIt
块)的地方使用。
Specify
替换It
的示范如下:
1 2 3 4 5 6 7 8 Describe("The foobar service" , func () { Context("when calling Foo()" , func () { Context("when no ID is provided" , func () { Specify("an ErrNoID error is returned" , func () { }) }) }) })
提取通用步骤:BeforeEach 您可以使用BeforeEach
块在多个测试用例中去除重复的步骤以及共享通用的设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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" )) }) })
BeforeEach
在每个 spec 之前运行,从而确保每个 spec 都具有状态的原始副本。使用闭包变量共享公共状态(在本例中为var book Book
)。您还可以在AfterEach
块中执行清理操作。
在BeforeEach
和AfterEach
块中设置断言也很常见。例如,这些断言,可以断言在为 spec 准备状态时没有发生错误。
存在容器嵌套时,最外层BeforeEach先运行。
AfterEach 多个Spec共享的、测试清理逻辑,可以放到AfterEach块中。存在容器嵌套时,最内层AfterEach先运行。
Describe/Context Ginkgo允许您使用Describe
和Context
容器在套件中富有表现力的组织 specs ,两者的区别:
Describe用于描述你的代码的一个行为
Context用于区分上述行为的不同情况,通常为参数不同导致
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 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 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 () { 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()) }) }) }) Describe("Extracting the author's last name" , func () { It("should correctly identify and return the last name" , func () { Expect(book.AuthorLastName()).To(Equal("Hugo" )) }) }) })
通常,容器块中的唯一代码应该是 It
块或 BeforeEach
/ JustBeforeEach
/ JustAfterEach
/ AfterEach
块或闭包变量声明。在容器块中进行断言通常是错误的。 在容器块中初始化闭包变量也是错误的。如果你的一个 It
改变了这个变量,后期 It
将会收到改变后的值。这是一个测试污染的案例,很难追查。始终在 BeforeEach
块中初始化变量。
分离创建和配置 JustBeforeEach 上面的例子说明了BDD风格测试中常见的反模式。我们的顶级 BeforeEach
使用有效的 JSON
创建了一个新的 book
,但是较低级别的 Context
使用无效的JSON创建的 book
执行。这使我们重新创建并覆盖原始的 book
。幸运的是,使用Ginkgo的 JustBeforeEach
块,这些代码重复是不必要的。
JustBeforeEach
块保证在所有 BeforeEach
块运行之后,并且在 It
块运行之前运行。我们可以使用这个特性来清除 Book
spec:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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) }) Describe("loading from JSON" , func () { Context("when the JSON parses succesfully" , func () { }) Context("when the JSON fails to parse" , func () { BeforeEach(func () { json = `{ "title":"Les Miserables", "author":"Victor Hugo", "pages":1488oops }` }) }) }) })
在上面的例子中,JustBeforeEach解耦了创建(Creation)和配置(Configuration)这两个阶段。现在,对每一个It
,book
实际上只创建一次。这个失败的JSON
上下文可以简单地将无效的json
值分配给BeforeEach
中的json
变量。
分离诊断收集和销毁 JustAfterEach 在销毁(可能会破坏有用的状态)之前,在每一个It块之后,有时运行一些代码是很有用的。比如,测试失败后,执行一些诊断的操作。我们可以在上面的示例中使用它来检查测试是否失败,如果失败,则输出实际的book
:
1 2 3 4 5 6 JustAfterEach(func () { if CurrentGinkgoTestDescription().Failed { fmt.Printf("Collecting diags just after failed test in %s\n" , CurrentGinkgoTestDescription().TestText) fmt.Printf("Actual book was %v\n" , book) } })
您可以在不同的嵌套级别使用多个JustAfterEach
。Ginkgo将首先从内到外运行所有JustAfterEach
,然后它将从内到外运行AfterEach
。虽然功能强大,但这会导致测试套件混乱 - 因此合理地使用嵌套的JustAfterEach
。
就像 JustBeforeEach
一样, JustAfterEach
是一个很容易被滥用的强大工具。好好利用它。
紧跟着It之后运行,在所有AfterEach执行之前。
全局设置和销毁 BeforeSuite/AfterSuite 有时您希望在整个测试之前运行一些设置代码和在整个测试之后运行一些清理代码。例如,您可能需要启动并销毁外部数据库。
Ginkgo提供了BeforeSuite
和AfterSuite
来实现这一点。通常,您可以在引导程序文件的顶层定义它们。例如,假设您需要设置外部数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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() })
BeforeSuite
函数在任何 spec运行之前运行。如果BeforeSuite
运行失败则没有 spec将会运行,测试套件运行结束。
AfterSuite
函数在所有的 spec运行之后运行,无论是否有任何测试的失败。由于AfterSuite
通常有一些代码来清理持久的状态,所以当你使用control+c
打断运行的测试时,Ginkgo也将会运行AfterSuite
。要退出AfterSuite
的运行,再次输入control+c
。
通过传递带有Done参数的函数,可以异步运行BeforeSuite
和AfterSuite
。
您只能在测试套件中定义一次BeforeSuite
和AfterSuite
(不需要设置多次 !)
最后,当并行运行时,每个并行进程都将运行BeforeSuite
和AfterSuite
函数。在这里 查看有关并行运行测试的更多信息。
记录复杂的It
: By
按照规则,您应该记录您的It
,BeforEach
, 等精炼到位。有时这是不可能的,特别是在集成式测试中测试复杂的工作流时。在这些情况下,您的测试块开始隐藏通过单独查看代码难以收集的叙述。在这些情况下,Ginkgo 通过By
来提供帮助,此块用于给逻辑复杂的块添加文档。这里有一个很好的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 var _ = Describe("Browsing the library" , func () { BeforeEach(func () { By("Fetching a token and logging in" ) authToken, err := authClient.GetToken("gopher" , "literati" ) Exepect(err).NotTo(HaveOccurred()) err := libraryClient.Login(authToken) Exepect(err).NotTo(HaveOccurred()) }) 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("Check the book out" ) err := libraryClient.CheckOut(book) Expect(err).NotTo(HaveOccurred()) books, err := aisle.GetBooks() Expect(books).To(HaveLen(6 )) Expect(books).NotTo(ContainElement(book)) }) })
传递给By的字符串是通过GinkgoWriter
发出的。如果测试成功,您将看不到Ginkgo绿点之外的任何输出。但是,如果测试失败,您将看到失败之前的每个步骤的打印输出。使用ginkgo -v
总是输出所有步骤打印。
By
采用一个可选的fun()
类型函数。当传入这样的一个函数时,By
将会立刻调用该函数。这将允许您组织您的多个It
到一组步骤,但这纯粹是可选的。在实际应用中,每个By
函数是一个单独的回调,这一特性限制了这种方法的可用性。
Spec Runner Pending Spec 你可以标记一个Spec或容器为Pending,这样默认情况下不会运行它们。定义块时使用P或X前缀:
1 2 3 4 5 6 7 8 9 PDescribe("some behavior" , func () { ... }) PContext("some scenario" , func () { ... }) PIt("some assertion" ) PMeasure("some measurement" ) XDescribe("some behavior" , func () { ... }) XContext("some scenario" , func () { ... }) XIt("some assertion" ) XMeasure("some measurement" )
默认情况下Ginkgo会为每个Pending的Spec打印描述信息,使用命令行选项 --noisyPendings=false
禁止该行为。
Skiping Spec P或X前缀会在编译期将Spec标记为Pending,你也可以在运行期跳过特定的Spec:
1 2 3 4 5 6 It("should do something, if it can" , func () { if !someCondition { Skip("special condition wasn't met" ) } })
Focused Specs 一个很常见的需求是,可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求:
将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:
1 2 3 FDescribe("some behavior" , func () { ... }) FContext("some scenario" , func () { ... }) FIt("some assertion" , func () { ... })
在命令行中传递正则式: —focus=REGEXP 或/和 —skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec
Parallel Specs Ginkgo支持并行的运行Spec,它实现方式是,创建go test子进程并在其中运行共享队列中的Spec。
使用 ginkgo -p可以启用并行测试,Ginkgo会自动创建适当数量的节点(进程)。你也可以指定节点数量: ginkgo -nodes=N。
如果你的测试代码需要和外部进程交互,或者创建外部进程,在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。
如果所有Spec需要共享一个外部进程,则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var _ = SynchronizedBeforeSuite(func () []byte { port := 4000 + config.GinkgoConfig.ParallelNode dbRunner = db.NewRunner() err := dbRunner.Start(port) Expect(err).NotTo(HaveOccurred()) return []byte (dbRunner.Address()) }, func (data []byte ) { dbAddress := string (data) dbClient = db.NewClient() err = dbClient.Connect(dbAddress) Expect(err).NotTo(HaveOccurred()) })
上面的例子,为所有节点创建共享的数据库,然后为每个节点创建独占的客户端。 SynchronizedAfterSuite的回调顺序则正好相反:
1 2 3 4 5 6 7 var _ = SynchronizedAfterSuite(func () { dbClient.Cleanup() }, func () { dbRunner.Stop() })
异步测试 在平时的代码中,我们经常会看到需要做异步处理的测试用例。但是这块的逻辑如果处理不好,用例可能会因为死锁或者未设置超时时间而异常卡住,非常的恼人。好在Ginkgo专门提供了原生的异步支持,能大大降低此类问题的风险。类似用法:
1 2 3 4 5 6 It("should post to the channel, eventually" , func () { c := make (chan string , 0 ) go DoSomething(c) Expect(<-c).To(ContainSubstring("Done!" )) })
这个测试会阻塞直到接受到通道c
的响应。对于这种测试,一个死锁或超时是常见的错误模式。对于这种情况,一个常见模式是在底部添加一个 select 语句 ,并包括一个<-time.After(X)
通道来指定超时。
Ginkgo 有这种内置模式。在所有无容器块(It
, BeforeEach
, AfterEach
, JustBeforeEach
, JustAfterEach
, 和 Benchmark
)中body
函数能接受一个可选的done Done
参数:
1 2 3 4 5 6 7 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 )
Done
是一个 chan interface{}
。当 Ginkgo 检测到 done Done
参数已经被请求了,它会运行 用 goroutine 运行 body
函数,并将它包裹到一个应用超时断言的必要逻辑中。你必须要么关闭 done
通道,要么发送一些东西(任何东西都行)给它来告诉 Ginkgo 你的测试已经结束。如果你的测试超时不结束,Ginkgo会让测试失败并进行下一个。
默认的超时是 1 秒。你可以在 body
函数后面传递一个 float64
(秒为单位)修改超时时间。
Gomega 对于丰富的异步代码断言有额外支持。确保查看了 Eventually
在 Gomega 是如何工作的。
针对分布式系统,我们在验收一些场景时,可能需要等待一段时间,目标结果才生效。而这个时间会因为不同集群负载而有所不同。所以简单的硬编码来sleep一个固定时间,很明显不合适。这种场景下若是使用Ginkgo对应的matcher库Gomega 的Eventually 功能就非常的贴切,在大大提升用例稳定性的同时,最大可能的减少无用的等待时间。
性能测试 使用Measure块可以进行性能测试,所有It能够出现的地方,都可以使用Measure。和It一样,Measure会生成一个新的Spec。
传递给Measure的闭包函数必须具有 Benchmarker 入参:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 )
执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。
CLI 运行测试 1 2 3 4 5 6 7 ginkgo ginkgo /path/to/package /path/to/other/package ... ginkgo -r ...
传递参数 传递参数给测试套件:
1 ginkgo -- PASS-THROUGHS-ARGS
跳过某些包 1 2 ginkgo -skipPackage=PACKAGES,TO,SKIP
超时控制 选项 -timeout
用于控制套件的最大运行时间,如果超过此时间仍然没有完成,认为测试失败。默认24小时。
调试信息
选项
说明
—reportPassed
打印通过的测试的详细信息
—v
冗长模式
—trace
打印所有错误的调用栈
—progress
打印进度信息
其他选项
选项
说明
-race
启用竞态条件检测
-cover
启用覆盖率测试
-tags
指定编译器标记
Gomega 这时Ginkgo推荐使用的断言(Matcher)库。
联用 和Ginkgo 1 gomega.RegisterFailHandler(ginkgo.Fail)
和Go测试框架 1 2 3 4 5 6 7 8 func TestFarmHasCow (t *testing.T) { g := NewGomegaWithT(t) f := farm.New([]string {"Cow" , "Horse" }) g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow" ) }
断言 Ω/Expect 两种断言语法本质是一样的,只是命名风格有些不同:
1 2 3 4 5 6 Ω(ACTUAL).Should(Equal(EXPECTED)) Expect(ACTUAL).To(Equal(EXPECTED)) Ω(ACTUAL).ShouldNot(Equal(EXPECTED)) Expect(ACTUAL).NotTo(Equal(EXPECTED)) Expect(ACTUAL).ToNot(Equal(EXPECTED))
错误处理 对于返回多个值的函数:
1 2 3 4 5 6 func DoSomethingHard () (string , error) {} result, err := DoSomethingHard() Ω(err).ShouldNot(HaveOccurred()) Ω(result).Should(Equal("foo" ))
对于仅仅返回一个error的函数:
1 2 3 func DoSomethingHard () (string , error) {} Ω(DoSomethingSimple()).Should(Succeed())
断言注解 进行断言时,可以提供格式化字符串,这样断言失败可以方便的知道原因:
1 2 3 4 5 Ω(ACTUAL).Should(Equal(EXPECTED), "My annotation %d" , foo) Expect(ACTUAL).To(Equal(EXPECTED), "My annotation %d" , foo) Expect(ACTUAL).To(Equal(EXPECTED), func () string { return "My annotation" })
简化输出 断言失败时,Gomega打印牵涉到断言的对象的递归信息,输出可能很冗长。
format包提供了一些全局变量,调整这些变量可以简化输出。
变量 = 默认值
说明
format.MaxDepth = 10
打印对象嵌套属性的最大深度
format.UseStringerRepresentation = false
默认情况下,Gomega不会调用Stringer.String()或GoStringer.GoString()方法来打印对象的字符串表示字符串表示通常人类可读但是信息量较小设置为true则打印字符串表示,可以简化输出
format.PrintContextObjects = false
默认情况下,Gomega不会打印context.Context接口的内容,因为通常非常冗长
format.TruncatedDiff = true
截断长字符串,仅仅打印差异
异步断言 Gomega提供了两个函数,用于异步断言。
传递给Eventually、Consistently的函数,如果返回多个值,则第一个返回值用于匹配,其它值断言为nil或零值。
Eventually 阻塞并轮询参数,直到能通过断言:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Eventually(func () []int { return thing.SliceImMonitoring }).Should(HaveLen(2 )) Eventually(channel).Should(BeClosed()) Eventually(channel).Should(Receive()) Eventually(myInstance.FetchNameFromNetwork).Should(Equal("archibald" )) Eventually(session).Should(gexec.Exit(0 )) Eventually(session.Out).Should(Say("Splines reticulated" ))
可以指定超时、轮询间隔:
1 2 3 Eventually(func () []int { return thing.SliceImMonitoring }, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2 ))
Consistently 检查断言是否在一定时间段内总是通过:
1 2 3 Consistently(func () []int { return thing.MemoryUsage() }, DURATION, POLLING_INTERVAL).Should(BeNumerically("<" , 10 ))
Consistently也可以用来断言最终不会发生的事件,例如下面的例子:
1 Consistently(channel).ShouldNot(Receive())
修改默认间隔 默认情况下,Eventually每10ms轮询一次,持续1s。Consistently每10ms轮询一次,持续100ms。调用下面的函数修改这些默认值:
1 2 3 4 SetDefaultEventuallyTimeout(t time .Duration) SetDefaultEventuallyPollingInterval(t time .Duration) SetDefaultConsistentlyDuration(t time .Duration) SetDefaultConsistentlyPollingInterval(t time .Duration)
这些调用会影响整个测试套件。
Matcher 相等性 1 2 3 4 5 6 7 8 9 10 Ω(ACTUAL).Should(Equal(EXPECTED)) Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED)) BeIdenticalTo(expected interface {})
接口相容 1 Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface ))
空值/零值 1 2 3 4 5 Ω(ACTUAL).Should(BeNil()) Ω(ACTUAL).Should(BeZero())
布尔值 1 2 Ω(ACTUAL).Should(BeTrue()) Ω(ACTUAL).Should(BeFalse())
错误 1 2 3 4 5 6 7 8 9 Ω(ACTUAL).Should(HaveOccurred()) err := SomethingThatMightFail() Ω(err).ShouldNot(HaveOccurred()) Ω(ACTUAL).Should(Succeed())
可以对错误进行细粒度的匹配:
1 Ω(ACTUAL).Should(MatchError(EXPECTED))
上面的EXPECTED可以是:
字符串:则断言ACTUAL.Error()与之相等
Matcher:则断言ACTUAL.Error()与之进行匹配
error:则ACTUAL和error基于reflect.DeepEqual()进行比较
实现了error接口的非Nil指针,调用 errors.As (ACTUAL, EXPECTED)进行检查
不符合以上条件的EXPECTED是不允许的。
通道 1 2 3 4 5 6 7 8 9 10 11 12 13 Ω(ACTUAL).Should(BeClosed()) Ω(ACTUAL).ShouldNot(BeClosed()) Ω(ACTUAL).Should(Receive(<optionalPointer>)) Ω(ACTUAL).Should(BeSent(VALUE))
文件 1 2 3 4 5 6 Ω(ACTUAL).Should(BeAnExistingFile()) Ω(ACTUAL).Should(BeARegularFile()) BeADirectory
字符串 1 2 3 4 5 6 7 8 9 10 11 12 Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...)) Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...)) Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...)) Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...))
JSON/XML/YAML 1 2 3 Ω(ACTUAL).Should(MatchJSON(EXPECTED)) Ω(ACTUAL).Should(MatchXML(EXPECTED)) Ω(ACTUAL).Should(MatchYAML(EXPECTED))
ACTUAL、EXPECTED可以是string、[]byte、Stringer。如果两者转换为对象是reflect.DeepEqual的则匹配。
集合 string, array, map, chan, slice都属于集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Ω(ACTUAL).Should(BeEmpty()) Ω(ACTUAL).Should(HaveLen(INT)) Ω(ACTUAL).Should(HaveCap(INT)) Ω(ACTUAL).Should(ContainElement(ELEMENT)) Ω(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...)) Ω(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...)) Ω(ACTUAL).Should(ConsistOf([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...})) Ω(ACTUAL).Should(HaveKey(KEY)) Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))
数字/时间 1 2 3 4 5 6 7 8 9 10 11 12 13 Ω(ACTUAL).Should(BeNumerically("==" , EXPECTED)) Ω(ACTUAL).Should(BeNumerically("~" , EXPECTED, <THRESHOLD>)) Ω(ACTUAL).Should(BeNumerically(">" , EXPECTED)) Ω(ACTUAL).Should(BeNumerically(">=" , EXPECTED)) Ω(ACTUAL).Should(BeNumerically("<" , EXPECTED)) Ω(ACTUAL).Should(BeNumerically("<=" , EXPECTED)) Ω(number).Should(BeBetween(0 , 10 ))
比较时间时使用BeTemporally函数,和BeNumerically类似。
Panic 断言会发生Panic:
1 Ω(ACTUAL).Should(Panic())
And/Or 1 2 3 4 5 6 7 8 9 10 11 12 13 Expect(number).To(SatisfyAll( BeNumerically(">" , 0 ), BeNumerically("<" , 10 ))) Expect(msg).To(And( Equal("Success" ), MatchRegexp(`^Error .+$` ))) Ω(ACTUAL).Should(SatisfyAny(MATCHER1, MATCHER2, ...)) Ω(ACTUAL).Should(Or(MATCHER1, MATCHER2, ...))
自定义Matcher 如果内置Matcher无法满足需要,你可以实现接口:
1 2 3 4 5 type GomegaMatcher interface { Match(actual interface {}) (success bool , err error) FailureMessage(actual interface {}) (message string ) NegatedFailureMessage(actual interface {}) (message string ) }
辅助工具 ghttp 用于测试HTTP客户端,此包提供了Mock HTTP服务器的能力。
gbytes gbytes.Buffer实现了接口io.WriteCloser,能够捕获到内存缓冲的输入。配合使用 gbytes.Say
能够对流数据进行有序的断言。
gexec 简化了外部进程的测试,可以:
编译Go二进制文件
启动外部进程
发送信号并等待外部进程退出
基于退出码进行断言
将输出流导入到gbytes.Buffer进行断言
gstruct 此包用于测试复杂的Go结构,提供了结构、切片、映射、指针相关的Matcher。
对所有字段进行断言:
1 2 3 4 5 6 7 8 9 10 actual := struct { A int B bool C string }{5 , true , "foo" } Expect(actual).To(MatchAllFields(Fields{ "A" : BeNumerically("<" , 10 ), "B" : BeTrue(), "C" : Equal("foo" ), })
不处理某些字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 Expect(actual).To(MatchFields(IgnoreExtras, Fields{ "A" : BeNumerically("<" , 10 ), "B" : BeTrue(), }) Expect(actual).To(MatchFields(IgnoreMissing, Fields{ "A" : BeNumerically("<" , 10 ), "B" : BeTrue(), "C" : Equal("foo" ), "D" : Equal("bar" ), })
一个复杂的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 coreID := func (element interface {}) string { return strconv.Itoa(element.(CoreStats).Index) } Expect(actual).To(MatchAllFields(Fields{ "Name" : Ignore(), "StartTime" : BeTemporally(">=" , time.Now().Add(-100 * time.Hour)), "CPU" : PointTo(MatchAllFields(Fields{ "Time" : BeTemporally(">=" , time.Now().Add(-time.Hour)), "UsageNanoCores" : BeNumerically("~" , 1E9 , 1E8 ), "UsageCoreNanoSeconds" : BeNumerically(">" , 1E6 ), "Cores" : MatchElements(coreID, IgnoreExtras, Elements{ "0" : MatchAllFields(Fields{ Index: Ignore(), "UsageNanoCores" : BeNumerically("<" , 1E9 ), "UsageCoreNanoSeconds" : BeNumerically(">" , 1E5 ), }), "1" : MatchAllFields(Fields{ Index: Ignore(), "UsageNanoCores" : BeNumerically("<" , 1E9 ), "UsageCoreNanoSeconds" : BeNumerically(">" , 1E5 ), }), }), })) "Logs" : m.Ignore(), }))
总结 自动化测试用例不应该仅仅是QA手中的工具,而应该尽可能多的作为业务验收服务,输出到CICD、灰度验证、线上验收等尽可能多的场景,以服务于整个业务线,利用 Ginkgo 我们可以很容易做到这一点:
CICD: 在定义suite时,使用RunSpecWithDefaultReporters
方法,可以让测试结果既输出到stdout,还可以输出一份Junit格式的报告。这样就可以通过类似Jenkins的工具方便的呈现测试结果,而不用任何其他的额外操作。
TaaS(Test as a Service): 通过ginkgo build
或者原生的go test -c
命令,可以方便的将测试用例,编译成 package.test
的二进制文件。如此的话,我们就可以方便的进行测试服务分发。典型的,如交付给SRE同学,辅助其应对线上灰度场景下的测试验收。
参考资料