Ginkgo学习笔记
Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发(BDD, Behavior-Driven Development)风格的测试框架,通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。
Ginkgo集成了Go语言的测试机制,你可以通过 go test来运行Ginkgo测试套件。
1 |
go get -u github.com/onsi/ginkgo/ginkgo |
假设我们想给books包编写Ginkgo测试,则首先需要使用命令创建一个Ginkgo test suite:
1 2 |
cd pkg/books ginkgo bootstrap |
上述命令会生成文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package books_test import ( // 使用点号导入,把这两个包导入到当前命名空间 . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "testing" ) func TestBooks(t *testing.T) { // 将Ginkgo的Fail函数传递给Gomega,Fail函数用于标记测试失败,这是Ginkgo和Gomega唯一的交互点 // 如果Gomega断言失败,就会调用Fail进行处理 RegisterFailHandler(Fail) // 启动测试套件 RunSpecs(t, "Books Suite") } |
现在,使用命令 ginkgo或者 go test即可执行测试套件。
上面的空测试套件没有什么价值,我们需要在此套接下编写测试(Spec)。虽然可以在books_suite_test.go中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。
执行命令 ginkgo generate book可以为源文件book.go生成测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package books_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" // 为了方便,被测试包被导入当前命名空间 . "ginkgo-study/pkg/books" ) // 顶级的Describe容器 // Describe块用于组织Specs,其中可以包含任意数量的: // BeforeEach:在Spec(It块)运行之前执行,嵌套Describe时最外层BeforeEach先执行 // AfterEach:在Spec运行之后执行,嵌套Describe时最内层AfterEach先执行 // JustBeforeEach:在It块,所有BeforeEach之后执行 // Measurement // 可以在Describe块内嵌套Describe、Context、When块 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 |
// 使用Describe、Context容器来组织Spec var _ = Describe("Book", func() { var ( // 通过闭包在BeforeEach和It之间共享数据 longBook Book shortBook Book ) // 此函数用于初始化Spec的状态,在It块之前运行。如果存在嵌套Describe,则最 // 外面的BeforeEach最先运行 BeforeEach(func() { longBook = Book{ Title: "Les Miserables", Author: "Victor Hugo", Pages: 1488, } shortBook = Book{ Title: "Fox In Socks", Author: "Dr. Seuss", Pages: 24, } }) Describe("Categorizing book length", func() { Context("With more than 300 pages", func() { // 通过It来创建一个Spec It("should be a novel", func() { // Gomega的Expect用于断言 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")) }) }) }) }) |
除了调用Gomega之外,你还可以调用Fail函数直接断言失败:
1 |
Fail("Failure reason") |
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() { // 如果doSomething返回false则下面的defer会确保从panic中恢复 defer GinkgoRecover() // Ω和Expect功能相同 Ω(doSomething()).Should(BeTrue()) // 在Goroutine中需要关闭done通道 close(done) }() }) |
全局的GinkgoWriter可以用于写日志。默认情况下GinkgoWriter仅仅在测试失败时将日志Dump到标准输出,以冗长模式( ginkgo -v 或 go test -ginkgo.v)运行Ginkgo时则会立即输出。
如果通过Ctrl + C中断测试,则Ginkgo会立即输出写入到GinkgoWriter的内容。联用 --progress则Ginkgo会在BeforeEach/It/AfterEach之前输出通知到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传递参数。
你可以在Describe、Context这两种容器块内编写Spec,每个Spec写在It块中。
为了贴合自然语言,可以使用It的别名Specify:
1 2 3 4 5 6 7 8 9 |
Describe("The foobar service", func() { Context("when calling Foo()", func() { Context("when no ID is provided", func() { // 应该返回ErrNoID错误 Specify("an ErrNoID error is returned", func() { }) }) }) }) |
多个Spec共享的、测试准备逻辑,可以放到BeforeEach块中。
在BeforeEach、AfterEach块中进行断言是允许的。
存在容器嵌套时,最外层BeforeEach先运行。
多个Spec共享的、测试清理逻辑,可以放到AfterEach块中。存在容器嵌套时,最内层AfterEach先运行。
两者的区别:
- 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 |
// 这是关于Book服务测试 var _ = Describe("Book", func() { var ( book Book err error ) BeforeEach(func() { book, err = NewBookFromJSON(`{ "title":"Les Miserables", "author":"Victor Hugo", "pages":1488 }`) }) // 测试加载Book行为 Describe("loading from JSON", func() { // 如果正常解析JSON 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()) }) }) // 如果无法解析JSON Context("when the JSON fails to parse", func() { BeforeEach(func() { // 这是一个BDD反模式,可以用JustBeforeEach 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")) }) }) }) |
上面的例子中,内层Spec需要尝试从无效JSON创建Book,因此它调用NewBookFromJSON对book变量进行覆盖。这种做法是推荐的,应该使用JustBeforeEach,这种块在任何BeforeEach执行完毕后执行:
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 ) // 准备默认JSON BeforeEach(func() { json = `{ "title":"Les Miserables", "author":"Victor Hugo", "pages":1488 }` }) JustBeforeEach(func() { // 按需,根据默认数据/无效JSON创建book,避免NewBookFromJSON的重复调用(如果代价很高的话……) 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为无效JSON json = `{ "title":"Les Miserables", "author":"Victor Hugo", "pages":1488oops }` }) }) }) }) |
在上面的例子中,JustBeforeEach解耦了创建(Creation)和配置(Configuration)这两个阶段。
紧跟着It之后运行,在所有AfterEach执行之前。
在整个测试套件执行之前/之后,进行准备/清理。和套件代码写在一起:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func TestBooks(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Books Suite") } var _ = BeforeSuite(func() { dbClient = db.NewClient() err = dbClient.Connect(dbRunner.Address()) Expect(err).NotTo(HaveOccurred()) }) var _ = AfterSuite(func() { dbClient.Cleanup() }) |
这两个块都支持异步执行,只需要给函数传递一个Done参数即可。
此块用于给逻辑复杂的块添加文档:
1 2 3 4 5 6 7 8 9 |
var _ = Describe("Browsing the library", func() { BeforeEach(func() { By("Fetching a token and logging in") }) It("should be a pleasant experience", func() { By("Entering an aisle") }) }) |
传递给By的字符串会发送给GinkgoWriter,如果测试失败你可以看到。
你可以传递一个可选的函数给By,此函数会立即执行。
使用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)) }) // 断言 执行时间 小于 0.2 秒 Ω(runtime.Seconds()).Should(BeNumerically("<", 0.2), "SomethingHard() shouldn't take too long.") // 录制任意数据 b.RecordValue("disk usage (in MB)", HowMuchDiskSpaceDidYouUse()) }, 10) |
执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。
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 | 指定编译器标记 |
你可以标记一个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禁止该行为。
P或X前缀会在编译期将Spec标记为Pending,你也可以在运行期跳过特定的Spec:
1 2 3 4 5 6 |
It("should do something, if it can", func() { if !someCondition { // 跳过此Spec,不需要Return语句 Skip("special condition wasn't met") } }) |
一个很常见的需求是,可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求:
- 将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:
123FDescribe("some behavior", func() { ... })FContext("some scenario", func() { ... })FIt("some assertion", func() { ... })
-
在命令行中传递正则式: --focus=REGEXP 或/和 --skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec
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推荐使用的断言(Matcher)库。
注册Fail处理器即可:
1 |
gomega.RegisterFailHandler(ginkgo.Fail) |
1 2 3 4 5 6 7 8 |
func TestFarmHasCow(t *testing.T) { // 创建Gomega对象 g := NewGomegaWithT(t) f := farm.New([]string{"Cow", "Horse"}) // 进行断言 g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") } |
两种断言语法本质是一样的,只是命名风格有些不同:
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或零值。
阻塞并轮询参数,直到能通过断言:
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")) // 可以和gexec包的Session配合 Eventually(session).Should(gexec.Exit(0)) // 命令最终应当以0退出 Eventually(session.Out).Should(Say("Splines reticulated")) // 检查标准输出 |
可以指定超时、轮询间隔:
1 2 3 |
Eventually(func() []int { return thing.SliceImMonitoring }, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2)) |
检查断言是否在一定时间段内总是通过:
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) |
这些调用会影响整个测试套件。
1 2 3 4 5 6 7 8 9 10 |
// 使用reflect.DeepEqual进行比较 // 如果ACTUAL和EXPECTED都为nil,断言会失败 Ω(ACTUAL).Should(Equal(EXPECTED)) // 先把ACTUAL转换为EXPECTED的类型,然后使用reflect.DeepEqual进行比较 // 应当避免用来比较数字 Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED)) // 使用 == 进行比较 BeIdenticalTo(expected interface{}) |
1 |
Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface)) |
1 2 3 4 5 |
// 断言ACTUAL为Nil Ω(ACTUAL).Should(BeNil()) // 断言ACTUAL为它的类型的零值,或者是Nil Ω(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为Nil则断言成功 Ω(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 14 15 16 |
// 断言通道是否关闭 // Gomega会尝试读取通道进行判断,因此你需要注意: // 如果是缓冲通道,你需要先将通道读干净 // 如果你后续需要再次读取通道,注意此断言的影响 Ω(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 |
// 子串判断 fmt.Sprintf(STRING, ARGS...) Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...)) // 前缀判断 Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...)) // 后缀判断 Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...)) // 正则式匹配 Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...)) |
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, ...})) // 断言存在指定的键,仅用于map Ω(ACTUAL).Should(HaveKey(KEY)) // 断言存在指定的键值对,仅用于map Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 断言数字意义(类型不感知)上的相等 Ω(ACTUAL).Should(BeNumerically("==", EXPECTED)) // 断言相似,无差不超过THRESHOLD(默认1e-8) Ω(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:
1 |
Ω(ACTUAL).Should(Panic()) |
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无法满足需要,你可以实现接口:
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) } |
用于测试HTTP客户端,此包提供了Mock HTTP服务器的能力。
gbytes.Buffer实现了接口io.WriteCloser,能够捕获到内存缓冲的输入。配合使用 gbytes.Say能够对流数据进行有序的断言。
简化了外部进程的测试,可以:
- 编译Go二进制文件
- 启动外部进程
- 发送信号并等待外部进程退出
- 基于退出码进行断言
- 将输出流导入到gbytes.Buffer进行断言
此包用于测试复杂的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(), // 忽略C字段 }) 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), // 包含匹配的元素, 抽取ID的函数 "Cores": MatchElements(coreID, IgnoreExtras, Elements{ // ID: Matcher "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(), })) |
[…] 一个行为驱动测试框架,参考:Ginkgo学习笔记 […]
good note! thanks for sharing