
测试CLI【免费下载链接】ginkgoA Modern Testing Framework for Go项目地址https://gitcode.com/gh_mirrors/gi/ginkgo点击查看免费下载Ginkgo 提供了完整的表驱动测试 DSLDescribeTable/Entry以及通过循环和数据动态生成 Specs 的惯用法让仅输入不同、结构重复的测试代码大幅收敛。本篇以官方技能文档 tables-and-dynamic-specs/SKILL.md 为骨架结合仓库中 table_dsl.go 源码与 table_test.go 集成测试完整讲解 Entry 参数求值时机、四种描述方式、装饰器、DescribeTableSubtree、fixture 加载与共享行为闭包读完即可写出可维护、可运行、可并行的表驱动测试。一切 Gotcha 的根源树构建期Tree Construction Phase理解 Ginkgo 表驱动测试的前提是牢记一个事实整套表格 DSL 只是语法糖全部在树构建期运行参见官方文档 docs/index.md#L1453-L1482 的 Table Specs are just Syntactic Sugar 小节。Ginkgo 将测试生命周期划分为两个阶段树构建期执行Describe/Context/It/DescribeTable等顶层调用搭建 Spec 树运行期逐个执行 Spec此时BeforeEach、BeforeSuite、It闭包才真正运行。DescribeTable在树构建期生成一个容器节点 每个 Entry 一个It节点BeforeEach等设置节点在运行期执行。所有接下来要讲的坑都源于 Entry 参数在树构建期就被求值。因此原文档的建议是能用DescribeTable/DescribeTableSubtree配合共享配置表达的场景优先于大量重复的It。DescribeTable Entry一个容器一行一个 SpecDescribeTable(desc, specFunc, ...Entry)会生成一个容器其中每个Entry对应一个It。Entry(desc, params...)的参数在运行期传给specFunc必须与specFunc的签名严格匹配——如果不匹配运行时会给出清晰的错误信息具体错误文案见下文的源码分析。DescribeTable(Extracting the authors first and last name, func(author string, isValid bool, firstName, lastName string) { book : books.Book{Title: My Book, Author: author, Pages: 10} Expect(book.IsValid()).To(Equal(isValid)) Expect(book.AuthorFirstName()).To(Equal(firstName)) Expect(book.AuthorLastName()).To(Equal(lastName)) }, Entry(both names, Victor Hugo, true, Victor, Hugo), Entry(one name, Hugo, true, , Hugo), Entry(no name, , false, , ), )嵌套与设置Table 也是普通容器由于DescribeTable本质上就是一个容器节点你可以把它嵌套进Describe/Context中并用BeforeEach包裹。官方文档展示了这种组合docs/index.md#L1536-L1567Describe(book, func() { var book *books.Book BeforeEach(func() { book books.Book{Title: Les Miserables, Author: Victor Hugo, Pages: 2783} Expect(book.IsValid()).To(BeTrue()) }) DescribeTable(Extracting the authors first and last name, func(author string, isValid bool, firstName, lastName string) { book.Author author Expect(book.IsValid()).To(Equal(isValid)) Expect(book.AuthorFirstName()).To(Equal(firstName)) Expect(book.AuthorLastName()).To(Equal(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 no name, , false, , ), ) })每个 Entry 生成的 Spec 运行前BeforeEach都会重新执行为 spec 闭包准备一份全新的book。等价展开Table 是 Describe It 的语法糖上面的表格测试等价于手写多个It。官方文档给出了完整展开docs/index.md#L1484-L1532核心差异在于表格版本把描述 参数集中在一处重复的测试逻辑收敛为一段闭包可读性与可维护性显著提升。最大的坑Entry 参数在树构建期求值Entry(...)的参数在树构建期任何BeforeEach运行之前被求值。因此Entry 不能读取BeforeEach中初始化的变量——它只能看到零值nil map / nil 指针。var shelf map[string]*books.Book BeforeEach(func() { shelf loadShelf() }) // 运行期才执行 // 错误 —— Entry 在树构建期求值时 shelf 还是 nil DescribeTable(category, func(b *books.Book, c books.Category) { ... }, Entry(novel, shelf[Les Miserables], books.CategoryNovel), // nil 指针 ) // 正确 —— 传 key在 spec 闭包运行期内再解引用 shelf DescribeTable(category, func(key string, c books.Category) { Expect(shelf[key].Category()).To(Equal(c)) }, Entry(novel, Les Miserables, books.CategoryNovel), )官方文档将此称为常见反例docs/index.md#L1571-L1618shelf[Les Miserables]在树构建期返回nil指针导致 Spec 运行期失败。解决办法是把对shelf的访问移入 spec 闭包让它在运行期、BeforeEach执行完之后再取值。Entry 描述的四种方式Entry 的描述最终会成为 Spec 的It文本失败时也会被打印因此值得认真对待。Ginkgo 提供四种生成描述的方式原文档用一张表总结机制用法显式字符串Entry(both names, ...)nilEntry(nil, 1, 2, 3)→ 按参数自动命名Entry: 1, 2, 3表级描述闭包把func(a,b,c int) string {...}作为DescribeTable的第 3 个参数渲染所有nil描述的 EntryEntryDescription(fmt)把EntryDescription(%d %d %d)作为DescribeTable第 3 个参数表级默认或作为单个 Entry 的第一参数按条目覆盖描述闭包必须返回string且参数签名与specFunc一致。按条目使用时Entry 的第一个参数本身可以是闭包或EntryDescription覆盖表级默认DescribeTable(addition, func(a, b, c int) { Expect(a b).To(Equal(c)) }, EntryDescription(%d %d %d), // 表级默认 Entry(nil, 1, 2, 3), // 1 2 3 Entry(zeros, 0, 0, 0), // 显式字符串 Entry(EntryDescription(%[3]d %[1]d %[2]d), 10, 100, 110), // 按条目覆盖 Entry(func(a, b, c int) string { return fmt.Sprintf(%d %d, ab, c) }, 4, 3, 7), )官方文档对应示例docs/index.md#L1623-L1731生成的名称分别为1 2 3、-1 2 1、zeros、110 10 100、7 7。源码视角描述如何被渲染在 table_dsl.go 的generateTable中可以清楚看到描述机制的底层实现tableLevelEntryDescription的默认实现是Entry: strings.Join(args, , )table_dsl.go#L208-L215这就是Entry(nil, ...)自动命名格式的来源当第 3 个参数是EntryDescription时会替换为fmt.Sprintf渲染table_dsl.go#L229-L230render方法见 table_dsl.go#L26-L28当第 3 个参数是返回单个string的函数时作为描述闭包使用table_dsl.go#L231-L232渲染每个 Entry 时按nil → 表级描述、EntryDescription → 格式串渲染、string → 原样、闭包 → 调用求值的顺序解析table_dsl.go#L249-L266。集成测试 table_test.go#L94-L257 覆盖了这些场景包括无表级描述时nilEntry 渲染为Entry: 1, b多个描述闭包时最后一个生效参数不匹配时以 panic 形式失败并报Too many parameters passed in to Entry Description function。装饰 Entry每个 Decorator 都可用Entry与DescribeTable接受所有Ginkgo 装饰器完整清单见 decorators/SKILL.md例如Entry(flaky case, FlakeAttempts(3), ...) // 允许重试 3 次 Entry(..., Label(slow)) // 打标签供 --label-filter 过滤 DescribeTable(..., Serial, ...) // 表级装饰器 DescribeTable(..., FlakeAttempts(2), ...) // 表级重试聚焦/挂起快捷方式同样存在FEntry/PEntry/XEntry以及表级FDescribeTable/PDescribeTable/XDescribeTable。注意PEntry不需要参数聚焦/挂起的优先级规则与其它节点一致参考 filtering。从源码看table_dsl.go#L165-L194Entry通过internal.PartitionDecorators把参数拆成装饰器与spec 参数两部分FEntry/PEntry只是额外追加internal.Focus/internal.Pending。集成测试验证了装饰器行为FlakeAttempts失败条目按配置重试table_test.go#L454-L494MustPassRepeatedly必须连续多次通过table_test.go#L496-L536PEntry与FEntry的挂起/聚焦语义table_test.go#L354-L452。DescribeTableSubtree每行生成一组 Spec当每个 Entry 需要一整个子树多个It 各自的设置节点时使用DescribeTableSubtree。它的 body 函数在树构建期、为每个 Entry 各执行一次且每次执行都发生在一个全新的容器内——因此你必须把It放在 body 内部否则不会生成任何 SpecDescribeTableSubtree(handling requests, func(url string, code int, message string) { var resp *http.Response BeforeEach(func() { var err error resp, err http.Get(url) Expect(err).NotTo(HaveOccurred()) DeferCleanup(resp.Body.Close) }) It(returns the status code, func() { Expect(resp.StatusCode).To(Equal(code)) }) It(returns the message, func() { body, _ : io.ReadAll(resp.Body) Expect(string(body)).To(Equal(message)) }) }, Entry(default, example.com/response, http.StatusOK, hello world), Entry(missing, example.com/missing, http.StatusNotFound, wat?), )官方文档给出了它的等价展开形式每个 Entry 对应一个Describe(url, ...)容器内部有自己的BeforeEach和两个Itdocs/index.md#L1764-L1809。源码视角Subtree 与普通 Table 的唯一区别在generateTable中DescribeTableSubtree只是把isSubtreetrue传入table_dsl.go#L112-L116。生成的内部节点类型随之改变DescribeTable→ 每个 Entry 生成types.NodeTypeIt节点table_dsl.go#L307-L310DescribeTableSubtree→ 每个 Entry 生成types.NodeTypeContainer容器节点body 闭包作为该容器的 bodytable_dsl.go#L308-L310。此外hasContext检测到 body 的第一个参数实现了SpecContext/context.Context时会给每个生成的It注入SpecContexttable_dsl.go#L271-L305从而支持NodeTimeout/SpecTimeout等中断型装饰器但Subtree 表不允许在 body 中使用SpecContext参数——检测到会直接报错ContextsCannotBeUsedInSubtreeTablestable_dsl.go#L295-L297。集成测试 table_test.go#L47-L92 展示了 Subtree 的行为每个 Entry 的BeforeEach独立运行多个It按定义顺序执行。实用模式Struct-per-row参数多时用结构体当一行参数过多时位置式 Entry 会变得难以阅读例如Entry(nil, 12, 1.2, 8.5, 11, 2783)。定义具名类型并传结构体Entry(nil, BookFormatting{FontSize: 12, LineHeight: 1.2, ...}, 2783)集成测试 table_test.go#L305-L339 中的ComplicatedThings结构体示例正是这种用法复杂类型作为参数被完整传递并在 spec 闭包中按值使用。可复用的 []TableEntry把一组 Entry 存入切片在多张表之间共享var InvalidBooks []TableEntry{ Entry(empty, books.Book{}), ... } DescribeTable(storing errors, storeFn, InvalidBooks) DescribeTable(reading errors, readFn, InvalidBooks)也可以把切片喂给DescribeTableSubtree为每个 Entry 附加多组 Spec。generateTable的源码明确支持[]TableEntry参数遇到reflect.TypeOf([]TableEntry{})时会把切片展开追加table_dsl.go#L227-L228测试 table_test.go#L24-L32 验证了混用单个 Entry 与切片的行为。加载 Fixture 数据放在 TestXxx 里而不是 BeforeSuite如果Spec 的结构依赖外部数据那么数据必须在树构建期就可获取。BeforeSuite在运行期执行——太晚了循环读取一个由BeforeSuite填充的切片会生成0 个 Spec。正确做法是在RunSpecs之前的TestXxx引导函数中加载var fixtureBooks []*books.Book func TestBooks(t *testing.T) { RegisterFailHandler(Fail) g : NewGomegaWithT(t) // 用 gomega 包装 t可在 RunSpecs 前断言 fixtureBooks LoadFixturesFrom(./fixtures/books.json) g.Expect(fixtureBooks).NotTo(BeEmpty()) RunSpecs(t, Books Suite) } var _ Describe(fixtures, func() { for _, book : range fixtureBooks { // 树构建前已填充 —— 有效 book : book It(stores book.Title, func() { Expect(library.Store(book)).To(Succeed()) }) } })这之所以可行是因为TestBooks先于树构建执行循环运行时fixtureBooks已被填充传给Describe的函数直到树构建期才被调用届时循环才真正展开。官方文档用同样的例子说明了为什么BeforeSuite方案无效docs/index.md#L4527-L4580BeforeSuite闭包在树构建期之后运行循环在它执行前就已经遍历了空切片。注意循环内book : book这一行它把循环变量拷贝到局部变量避免It闭包捕获被后续迭代修改的循环变量否则所有 Spec 都会针对最后一个元素运行。共享行为Shared Behaviors跨 Context 复用同一组 It当多个Context只有BeforeEach不同、It完全相同时可以把It提取到闭包中并在每个Contextbody 内调用它。由于闭包与共享变量定义在同一作用域它会闭包捕获各Context的BeforeEach所配置的变量AssertFailedBehavior : func() { It(cant be stored, func() { Expect(library.IsStorable(book)).To(BeFalse()) }) It(fails to store, func() { Expect(library.Store(book)).To(MatchError(books.ErrStoringBook)) }) } Context(when the book has no title, func() { BeforeEach(func() { book books.Book{Author: Victor Hugo, Pages: 2783} }) AssertFailedBehavior() }) Context(when the book is nil, func() { BeforeEach(func() { book nil }) AssertFailedBehavior() })AssertFailedBehavior在树构建期被调用把两个It注入各自所在的 Context。官方文档的对应示例见 docs/index.md#L4582-L4640。补充源码中的参数校验与错误信息Entry参数与specFunc签名不匹配时Ginkgo 通过validateParameterstable_dsl.go#L342-L378在树构建期做反射校验并生成明确的运行时错误。集成测试验证了这些消息table_test.go#L259-L302参数过少The Table Body function expected 2 parameters but you passed in 1参数类型错误The Table Body function expected parameter #2 to be of type string but you passed in int变参类型错误The Table Body function expected its variadic parameters to be of type float64 but you passed in int变参参数合法func(a int, b string, c ...float64)可接受Entry(nil, 1, b, 2.71, 3.141)nil 参数合法Entry(nils, nil, nil)会以零值传入。需要完整调用链时可继续阅读根目录 table_dsl.go若只想按需 dot-import 表格 DSL 以避免与既有符号冲突Ginkgo 还提供了独立的 dsl/table/table_dsl.go 子包别名导出全部表格相关类型与函数。小结Ginkgo 表驱动测试的核心心智模型只有一条表格 DSL 在树构建期展开Entry 参数在树构建期求值。牢记这一点即可规避绝大多数陷阱——不在 Entry 中引用BeforeEach变量、把影响 Spec 结构的数据加载提前到TestXxx、用闭包在多个 Context 间共享It。在此之上EntryDescription、装饰器、[]TableEntry复用与DescribeTableSubtree能进一步把重复代码压缩到极致让测试既简洁又具备完整的报告、过滤与重试能力。赞分享测试CLI【免费下载链接】ginkgoA Modern Testing Framework for Go项目地址https://gitcode.com/gh_mirrors/gi/ginkgo点击查看免费下载相关推荐深入RetinaFace源码解析TensorFlow人脸检测模型实现原理深入RetinaFace源码解析TensorFlow人脸检测模型实现原理 RetinaFace是一个基于TensorFlow的深度学习人脸检测库它能够高效准计算机视觉深度学习v-charts数据更新终极指南5种动态刷新图表的正确姿势v charts数据更新终极指南5种动态刷新图表的正确姿势 v charts是基于Vue2.0和ECharts封装的图表组件库为开发者提供了简洁易用的数据可前端数据可视化UI组件DevOps-Python-tools Hive/Impala元数据管理表统计、行数、列数自动分析DevOps Python tools Hive/Impala元数据管理表统计、行数、列数自动分析 DevOps Python tools是一个集成80实用上一篇Rust Rosetta Code图形编程使用OpenGL和位图处理的实战技巧下一篇up/up的Lambda版本部署渐进式创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考