Ginkgo 测试框架
Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发(BDD, Behavior-Driven Development)风格的测试框架,通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。Ginkgo 集成了Go语言的测试机制,你可以通过 go test 来运行Ginkgo测试套件。本文所有的代码可以在我的 Github 中找到。
技术特点
Behavior Driven Development
Ginkgo最大的特点就是对BDD风格的支持。比如:
|
|
Ginkgo 定义的 DSL 语法(Describe/Context/It)可以非常方便的帮助大家组织和编排测试用例。在BDD模式中,测试用例的标题书写,要非常注意表达,要能清晰的指明用例测试的业务场景。只有这样才能极大的增强用例的可读性,降低使用和维护的心智负担。
可读性这一点,在自动化测试用例设计原则上,非常重要。因为测试用例不同于一般意义上的程序,它在绝大部分场景下,看起来都像是一段段独立的方法,每个方法背后隐藏的业务逻辑也是细小的,不具通识性。这个问题在用例量少的情况下,还不明显。但当用例数量上到一定量级,你会发现,如果能快速理解用例到底是能做什么的,真的非常重要。而这正是 BDD 能补足的地方。
不过还是要强调,Ginkgo 只是提供对 BDD 模式的支持,你的用例最终呈现的效果,还是依赖你自己的书写。
进程级并行,稳定高效
相应的我们知道,BDD 框架,因为其DSL的深度嵌套支持,会存在一些共享上下文的资源,如此的话想做线程级的并发会比较困难。而Ginkgo巧妙的避开了这个问题,它通过在运行时,运行多个被测服务的进程,来达到真正的并行,稳定性大大提高。其使用姿势也非常简单,ginkgo -p命令就可以。在实践中,我们通常使用32核以上的服务器来跑集测,执行效率非常高。
这里有个细节,Ginkgo 虽然并行执行测试用例,但其输出的日志和测试报告格式,仍然是整齐不错乱的,这是如何做到的呢?原来,通过源码会发现,ginkgo CLI 工具在并行跑用例时,其内部会起一个监听随机端口的本地服务器,来做不同进程之间的消息同步,以及日志和报告的聚合工作,是不是很巧妙?
使用实战
安装
|
|
起步
创建套件
假设我有一个 books 的包,当前非常简单,有函数如下:
|
|
假设我们想给 books 包编写 Ginkgo 测试,则首先需要使用命令创建一个Ginkgo test suite:
|
|
上述命令会生成文件:
|
|
现在,使用命令 ginkgo或者 go test即可执行测试套件。
|
|
添加Spec
上面的空测试套件没有什么价值,我们需要在此套接下编写测试(Spec)。虽然可以在 books_suite_test.go 中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。
执行命令 ginkgo generate book 可以为源文件 books.go 生成测试:
|
|
我们可以添加一些Specs:
|
|
运行上述测试显示如下:
|
|
断言失败
除了调用Gomega之外,你还可以调用Fail函数直接断言失败:
|
|
Fail会记录当前进行的测试,并且触发panic,当前Spec的后续断言不会再进行。
通常情况下Ginkgo会 **从panic中恢复,并继续下一个测试 **。但是,如果你启动了一个Goroutine,并在其中触发了断言失败,则不会自动恢复,必须手工调用GinkgoRecover:
|
|
记录日志
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包即可:
|
|
执行测试时使用 ginkgo -- --myFlag=xxx 传递参数。
测试结构
单个Specs: It
你可以在Describe、Context这两种容器块内编写Spec,每个Spec写在It块中。
您可以通过在Describe或Context容器块中设置 It 块来添加单个 spec:
|
|
It也可以放在顶层,虽然这种情况并不常见。
Specify 别名
为了确保您的 specs 阅读自然,Specify,PSpecify,XSpecify和FSpecify块可用作别名,以便在相应的It替代品看起来不像自然语言的情况下使用。
Specify块的行为与It块相同,可以在It块(以及PIt,XIt和FIt块)的地方使用。
Specify替换It的示范如下:
|
|
提取通用步骤:BeforeEach
您可以使用BeforeEach块在多个测试用例中去除重复的步骤以及共享通用的设置:
|
|
BeforeEach在每个 spec 之前运行,从而确保每个 spec 都具有状态的原始副本。使用闭包变量共享公共状态(在本例中为var book Book)。您还可以在AfterEach块中执行清理操作。
在BeforeEach和AfterEach块中设置断言也很常见。例如,这些断言,可以断言在为 spec 准备状态时没有发生错误。
存在容器嵌套时,最外层BeforeEach先运行。
AfterEach
多个Spec共享的、测试清理逻辑,可以放到AfterEach块中。存在容器嵌套时,最内层AfterEach先运行。
Describe/Context
Ginkgo允许您使用Describe和Context容器在套件中富有表现力的组织 specs ,两者的区别:
- Describe用于描述你的代码的一个行为
- Context用于区分上述行为的不同情况,通常为参数不同导致
下面是一个例子:
|
|
通常,容器块中的唯一代码应该是
It块或BeforeEach/JustBeforeEach/JustAfterEach/AfterEach块或闭包变量声明。在容器块中进行断言通常是错误的。 在容器块中初始化闭包变量也是错误的。如果你的一个It改变了这个变量,后期It将会收到改变后的值。这是一个测试污染的案例,很难追查。始终在BeforeEach块中初始化变量。
分离创建和配置 JustBeforeEach
上面的例子说明了BDD风格测试中常见的反模式。我们的顶级 BeforeEach 使用有效的 JSON 创建了一个新的 book ,但是较低级别的 Context 使用无效的JSON创建的 book 执行。这使我们重新创建并覆盖原始的 book 。幸运的是,使用Ginkgo的 JustBeforeEach 块,这些代码重复是不必要的。
JustBeforeEach 块保证在所有 BeforeEach 块运行之后,并且在 It 块运行之前运行。我们可以使用这个特性来清除 Book spec:
|
|
在上面的例子中,JustBeforeEach解耦了创建(Creation)和配置(Configuration)这两个阶段。现在,对每一个It,book实际上只创建一次。这个失败的JSON上下文可以简单地将无效的json值分配给BeforeEach中的json变量。
分离诊断收集和销毁 JustAfterEach
在销毁(可能会破坏有用的状态)之前,在每一个It块之后,有时运行一些代码是很有用的。比如,测试失败后,执行一些诊断的操作。我们可以在上面的示例中使用它来检查测试是否失败,如果失败,则输出实际的book:
|
|
您可以在不同的嵌套级别使用多个
JustAfterEach。Ginkgo将首先从内到外运行所有JustAfterEach,然后它将从内到外运行AfterEach。虽然功能强大,但这会导致测试套件混乱 - 因此合理地使用嵌套的JustAfterEach。就像
JustBeforeEach一样,JustAfterEach是一个很容易被滥用的强大工具。好好利用它。
紧跟着It之后运行,在所有AfterEach执行之前。
全局设置和销毁 BeforeSuite/AfterSuite
有时您希望在整个测试之前运行一些设置代码和在整个测试之后运行一些清理代码。例如,您可能需要启动并销毁外部数据库。
Ginkgo提供了BeforeSuite和AfterSuite来实现这一点。通常,您可以在引导程序文件的顶层定义它们。例如,假设您需要设置外部数据库:
|
|
BeforeSuite 函数在任何 spec运行之前运行。如果BeforeSuite运行失败则没有 spec将会运行,测试套件运行结束。
AfterSuite函数在所有的 spec运行之后运行,无论是否有任何测试的失败。由于AfterSuite通常有一些代码来清理持久的状态,所以当你使用control+c 打断运行的测试时,Ginkgo也将会运行AfterSuite。要退出AfterSuite的运行,再次输入control+c。
通过传递带有Done参数的函数,可以异步运行BeforeSuite和AfterSuite。
您只能在测试套件中定义一次BeforeSuite和AfterSuite(不需要设置多次!)
最后,当并行运行时,每个并行进程都将运行BeforeSuite和AfterSuite函数。在[这里](https://ke-chain.github.io/ginkgodoc/#并行 specs)查看有关并行运行测试的更多信息。
记录复杂的It: By
按照规则,您应该记录您的It,BeforEach, 等精炼到位。有时这是不可能的,特别是在集成式测试中测试复杂的工作流时。在这些情况下,您的测试块开始隐藏通过单独查看代码难以收集的叙述。在这些情况下,Ginkgo 通过By来提供帮助,此块用于给逻辑复杂的块添加文档。这里有一个很好的例子:
|
|
传递给By的字符串是通过GinkgoWriter发出的。如果测试成功,您将看不到Ginkgo绿点之外的任何输出。但是,如果测试失败,您将看到失败之前的每个步骤的打印输出。使用ginkgo -v总是输出所有步骤打印。
By 采用一个可选的fun()类型函数。当传入这样的一个函数时,By将会立刻调用该函数。这将允许您组织您的多个It到一组步骤,但这纯粹是可选的。在实际应用中,每个By函数是一个单独的回调,这一特性限制了这种方法的可用性。
Spec Runner
Pending Spec
你可以标记一个Spec或容器为Pending,这样默认情况下不会运行它们。定义块时使用P或X前缀:
|
|
默认情况下Ginkgo会为每个Pending的Spec打印描述信息,使用命令行选项 --noisyPendings=false 禁止该行为。
Skiping Spec
P或X前缀会在编译期将Spec标记为Pending,你也可以在运行期跳过特定的Spec:
|
|
Focused Specs
一个很常见的需求是,可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求:
将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:
|
|
在命令行中传递正则式: –focus=REGEXP 或/和 –skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec
Parallel Specs
Ginkgo支持并行的运行Spec,它实现方式是,创建go test子进程并在其中运行共享队列中的Spec。
使用 ginkgo -p可以启用并行测试,Ginkgo会自动创建适当数量的节点(进程)。你也可以指定节点数量: ginkgo -nodes=N。
如果你的测试代码需要和外部进程交互,或者创建外部进程,在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。
如果所有Spec需要共享一个外部进程,则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite:
|
|
上面的例子,为所有节点创建共享的数据库,然后为每个节点创建独占的客户端。 SynchronizedAfterSuite的回调顺序则正好相反:
|
|
异步测试
在平时的代码中,我们经常会看到需要做异步处理的测试用例。但是这块的逻辑如果处理不好,用例可能会因为死锁或者未设置超时时间而异常卡住,非常的恼人。好在Ginkgo专门提供了原生的异步支持,能大大降低此类问题的风险。类似用法:
|
|
这个测试会阻塞直到接受到通道c的响应。对于这种测试,一个死锁或超时是常见的错误模式。对于这种情况,一个常见模式是在底部添加一个 select 语句 ,并包括一个<-time.After(X)通道来指定超时。
Ginkgo 有这种内置模式。在所有无容器块(It, BeforeEach, AfterEach, JustBeforeEach, JustAfterEach, 和 Benchmark)中body函数能接受一个可选的done Done 参数:
|
|
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 入参:
|
|
执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。
CLI
运行测试
|
|
传递参数
传递参数给测试套件:
|
|
跳过某些包
|
|
超时控制
选项 -timeout 用于控制套件的最大运行时间,如果超过此时间仍然没有完成,认为测试失败。默认24小时。
调试信息
| 选项 | 说明 |
|---|---|
| –reportPassed | 打印通过的测试的详细信息 |
| –v | 冗长模式 |
| –trace | 打印所有错误的调用栈 |
| –progress | 打印进度信息 |
其他选项
| 选项 | 说明 |
|---|---|
| -race | 启用竞态条件检测 |
| -cover | 启用覆盖率测试 |
| -tags | 指定编译器标记 |
Gomega
这时Ginkgo推荐使用的断言(Matcher)库。
联用
和Ginkgo
|
|
和Go测试框架
|
|
断言
Ω/Expect
两种断言语法本质是一样的,只是命名风格有些不同:
|
|
错误处理
对于返回多个值的函数:
|
|
对于仅仅返回一个error的函数:
|
|
断言注解
进行断言时,可以提供格式化字符串,这样断言失败可以方便的知道原因:
|
|
简化输出
断言失败时,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
阻塞并轮询参数,直到能通过断言:
|
|
可以指定超时、轮询间隔:
|
|
Consistently
检查断言是否在一定时间段内总是通过:
|
|
Consistently也可以用来断言最终不会发生的事件,例如下面的例子:
|
|
修改默认间隔
默认情况下,Eventually每10ms轮询一次,持续1s。Consistently每10ms轮询一次,持续100ms。调用下面的函数修改这些默认值:
SetDefaultEventuallyTimeout(t time.Duration)
SetDefaultEventuallyPollingInterval(t time.Duration)
SetDefaultConsistentlyDuration(t time.Duration)
SetDefaultConsistentlyPollingInterval(t time.Duration)
这些调用会影响整个测试套件。
Matcher
相等性
|
|
接口相容
|
|
空值/零值
|
|
布尔值
|
|
错误
|
|
可以对错误进行细粒度的匹配:
|
|
上面的EXPECTED可以是:
- 字符串:则断言ACTUAL.Error()与之相等
- Matcher:则断言ACTUAL.Error()与之进行匹配
- error:则ACTUAL和error基于reflect.DeepEqual()进行比较
- 实现了error接口的非Nil指针,调用 errors.As(ACTUAL, EXPECTED)进行检查
不符合以上条件的EXPECTED是不允许的。
通道
|
|
文件
|
|
字符串
|
|
JSON/XML/YAML
|
|
ACTUAL、EXPECTED可以是string、[]byte、Stringer。如果两者转换为对象是reflect.DeepEqual的则匹配。
集合
string, array, map, chan, slice都属于集合。
|
|
数字/时间
|
|
比较时间时使用BeTemporally函数,和BeNumerically类似。
Panic
断言会发生Panic:
|
|
And/Or
|
|
自定义Matcher
如果内置Matcher无法满足需要,你可以实现接口:
|
|
辅助工具
ghttp
用于测试HTTP客户端,此包提供了Mock HTTP服务器的能力。
gbytes
gbytes.Buffer实现了接口io.WriteCloser,能够捕获到内存缓冲的输入。配合使用 gbytes.Say 能够对流数据进行有序的断言。
gexec
简化了外部进程的测试,可以:
- 编译Go二进制文件
- 启动外部进程
- 发送信号并等待外部进程退出
- 基于退出码进行断言
- 将输出流导入到gbytes.Buffer进行断言
gstruct
此包用于测试复杂的Go结构,提供了结构、切片、映射、指针相关的Matcher。
对所有字段进行断言:
|
|
不处理某些字段:
|
|
一个复杂的例子:
|
|
总结
自动化测试用例不应该仅仅是QA手中的工具,而应该尽可能多的作为业务验收服务,输出到CICD、灰度验证、线上验收等尽可能多的场景,以服务于整个业务线,利用 Ginkgo 我们可以很容易做到这一点:
- CICD: 在定义suite时,使用
RunSpecWithDefaultReporters方法,可以让测试结果既输出到stdout,还可以输出一份Junit格式的报告。这样就可以通过类似Jenkins的工具方便的呈现测试结果,而不用任何其他的额外操作。 - TaaS(Test as a Service): 通过
ginkgo build或者原生的go test -c命令,可以方便的将测试用例,编译成package.test的二进制文件。如此的话,我们就可以方便的进行测试服务分发。典型的,如交付给SRE同学,辅助其应对线上灰度场景下的测试验收。
参考资料
-
No backlinks found.