Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Ginkgo学习笔记

19
Nov
2019

Ginkgo学习笔记

By Alex
/ in Go
/ tags 单元测试
2 Comments
简介

Ginkgo /ˈɡɪŋkoʊ / 是Go语言的一个行为驱动开发(BDD, Behavior-Driven Development)风格的测试框架,通常和库Gomega一起使用。Ginkgo在一系列的“Specs”中描述期望的程序行为。

Ginkgo集成了Go语言的测试机制,你可以通过 go test来运行Ginkgo测试套件。

Ginkgo
安装
Shell
1
go get -u github.com/onsi/ginkgo/ginkgo
起步
创建套件

假设我们想给books包编写Ginkgo测试,则首先需要使用命令创建一个Ginkgo test suite:

Shell
1
2
cd pkg/books
ginkgo bootstrap

上述命令会生成文件:

pkg/books/books_suite_test.go
Go
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

上面的空测试套件没有什么价值,我们需要在此套接下编写测试(Spec)。虽然可以在books_suite_test.go中编写测试,但是推荐分离到独立的文件中,特别是包中有多个需要被测试的源文件的情况下。

执行命令 ginkgo generate book可以为源文件book.go生成测试:

pkg/books/book_test.go
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:

Go
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函数直接断言失败:

Go
1
Fail("Failure reason")

Fail会记录当前进行的测试,并且触发panic,当前Spec的后续断言不会再进行。

通常情况下Ginkgo会从panic中恢复,并继续下一个测试。但是,如果你启动了一个Goroutine,并在其中触发了断言失败,则不会自动恢复,必须手工调用GinkgoRecover:

Go
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包即可:

Go
1
2
3
4
var myFlag string
func init() {
    flag.StringVar(&myFlag, "myFlag", "defaultvalue", "myFlag is used to control my behavior")
}

执行测试时使用 ginkgo -- --myFlag=xxx传递参数。

测试的结构
It

你可以在Describe、Context这两种容器块内编写Spec,每个Spec写在It块中。

为了贴合自然语言,可以使用It的别名Specify:

Go
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() {
      })
    })
  })
})
BeforeEach

多个Spec共享的、测试准备逻辑,可以放到BeforeEach块中。

在BeforeEach、AfterEach块中进行断言是允许的。

存在容器嵌套时,最外层BeforeEach先运行。

AfterEach

多个Spec共享的、测试清理逻辑,可以放到AfterEach块中。存在容器嵌套时,最内层AfterEach先运行。

Describe/Context

两者的区别:

  1. Describe用于描述你的代码的一个行为
  2. Context用于区分上述行为的不同情况,通常为参数不同导致

下面是一个例子:

Go
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"))
        })
    })
})
JustBeforeEach

上面的例子中,内层Spec需要尝试从无效JSON创建Book,因此它调用NewBookFromJSON对book变量进行覆盖。这种做法是推荐的,应该使用JustBeforeEach,这种块在任何BeforeEach执行完毕后执行:

Go
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)这两个阶段。

JustAfterEach

紧跟着It之后运行,在所有AfterEach执行之前。

BeforeSuite/AfterSuite

在整个测试套件执行之前/之后,进行准备/清理。和套件代码写在一起:

Go
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参数即可。 

By

此块用于给逻辑复杂的块添加文档:

Go
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入参:

Go
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)

执行时间、你录制的任意数据的最小、最大、平均值均会在测试完毕后打印出来。

CLI
运行测试
Shell
1
2
3
4
5
6
7
# 运行当前目录中的测试
ginkgo
# 运行其它目录中的测试
ginkgo /path/to/package /path/to/other/package ...
 
# 递归运行所有子目录中的测试
ginkgo -r ...
传递参数

传递参数给测试套件:  

Shell
1
ginkgo -- PASS-THROUGHS-ARGS
跳过某些包
Go
1
2
# 跳过某些包
ginkgo -skipPackage=PACKAGES,TO,SKIP
超时控制

选项 -timeout用于控制套件的最大运行时间,如果超过此时间仍然没有完成,认为测试失败。默认24小时。

调试信息
选项 说明
--reportPassed 打印通过的测试的详细信息
--v 冗长模式
--trace 打印所有错误的调用栈
--progress 打印进度信息
其它选项
选项 说明
-race 启用竞态条件检测
-cover 启用覆盖率测试
-tags 指定编译器标记
Spec Runner
Pending Spec

你可以标记一个Spec或容器为Pending,这样默认情况下不会运行它们。定义块时使用P或X前缀:

Go
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:

Go
1
2
3
4
5
6
It("should do something, if it can", func() {
    if !someCondition {
        // 跳过此Spec,不需要Return语句
        Skip("special condition wasn't met")
    }
})
Focused Specs

一个很常见的需求是,可以选择运行Spec的一个子集。Ginkgo提供两种机制满足此需求:

  1.  将容器或Spec标记为Focused,这样默认情况下Ginkgo仅仅运行Focused Spec:
    Go
    1
    2
    3
    FDescribe("some behavior", func() { ... })
    FContext("some scenario", func() { ... })
    FIt("some assertion", func() { ... })
  1. 在命令行中传递正则式: --focus=REGEXP 或/和 --skip=REGEXP,则Ginkgo仅仅运行/跳过匹配的Spec

Parallel Specs

Ginkgo支持并行的运行Spec,它实现方式是,创建go test子进程并在其中运行共享队列中的Spec。

使用 ginkgo -p可以启用并行测试,Ginkgo会自动创建适当数量的节点(进程)。你也可以指定节点数量: ginkgo -nodes=N。

如果你的测试代码需要和外部进程交互,或者创建外部进程,在并行测试上下文中需要谨慎的处理。最简单的方式是在BeforeSuite方法中为每个节点创建外部资源。

如果所有Spec需要共享一个外部进程,则可以利用SynchronizedBeforeSuite、SynchronizedAfterSuite:

Go
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的回调顺序则正好相反:

Go
1
2
3
4
5
6
7
var _ = SynchronizedAfterSuite(func() {
    // 所有节点
    dbClient.Cleanup()
}, func() {
    // 第一个节点
    dbRunner.Stop()
}) 
Gomega

这时Ginkgo推荐使用的断言(Matcher)库。

联用
和Ginkgo

注册Fail处理器即可:

Go
1
gomega.RegisterFailHandler(ginkgo.Fail)
和Go测试框架
Go
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")
}
断言
Ω/Expect

两种断言语法本质是一样的,只是命名风格有些不同:

Go
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))
错误处理

对于返回多个值的函数:

Go
1
2
3
4
5
6
func DoSomethingHard() (string, error) {}
 
result, err := DoSomethingHard()
// 断言没有发生错误
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal("foo"))

对于仅仅返回一个error的函数: 

Go
1
2
3
func DoSomethingHard() (string, error) {}
 
Ω(DoSomethingSimple()).Should(Succeed())
断言注解

进行断言时,可以提供格式化字符串,这样断言失败可以方便的知道原因:

Go
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

阻塞并轮询参数,直到能通过断言:

Go
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")) // 检查标准输出

可以指定超时、轮询间隔:

Go
1
2
3
Eventually(func() []int {
    return thing.SliceImMonitoring
}, TIMEOUT, POLLING_INTERVAL).Should(HaveLen(2))
Consistently

检查断言是否在一定时间段内总是通过:

Go
1
2
3
Consistently(func() []int {
    return thing.MemoryUsage()
}, DURATION, POLLING_INTERVAL).Should(BeNumerically("<", 10))

Consistently也可以用来断言最终不会发生的事件,例如下面的例子:

Go
1
Consistently(channel).ShouldNot(Receive())
修改默认间隔

默认情况下,Eventually每10ms轮询一次,持续1s。Consistently每10ms轮询一次,持续100ms。调用下面的函数修改这些默认值:

Go
1
2
3
4
SetDefaultEventuallyTimeout(t time.Duration)
SetDefaultEventuallyPollingInterval(t time.Duration)
SetDefaultConsistentlyDuration(t time.Duration)
SetDefaultConsistentlyPollingInterval(t time.Duration)

这些调用会影响整个测试套件。

内置Matcher
相等性 
Go
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{})
接口相容
Go
1
Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface))
空值/零值
Go
1
2
3
4
5
// 断言ACTUAL为Nil
Ω(ACTUAL).Should(BeNil())
 
// 断言ACTUAL为它的类型的零值,或者是Nil
Ω(ACTUAL).Should(BeZero())
布尔值
Go
1
2
Ω(ACTUAL).Should(BeTrue())
Ω(ACTUAL).Should(BeFalse())
错误
Go
1
2
3
4
5
6
7
8
9
Ω(ACTUAL).Should(HaveOccurred())
 
err := SomethingThatMightFail()
// 没有错误
Ω(err).ShouldNot(HaveOccurred())
 
 
// 如果ACTUAL为Nil则断言成功
Ω(ACTUAL).Should(Succeed())

可以对错误进行细粒度的匹配:

Go
1
Ω(ACTUAL).Should(MatchError(EXPECTED))

上面的EXPECTED可以是:

  1. 字符串:则断言ACTUAL.Error()与之相等
  2. Matcher:则断言ACTUAL.Error()与之进行匹配
  3. error:则ACTUAL和error基于reflect.DeepEqual()进行比较
  4. 实现了error接口的非Nil指针,调用 errors.As(ACTUAL, EXPECTED)进行检查

不符合以上条件的EXPECTED是不允许的。

通道
Go
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))
文件
Go
1
2
3
4
5
6
// 文件或目录存在
Ω(ACTUAL).Should(BeAnExistingFile())
// 断言是普通文件
Ω(ACTUAL).Should(BeARegularFile())
// 断言是目录
BeADirectory
字符串
Go
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...))
JSON/XML/YML
Go
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都属于集合。

Go
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))
数字/时间
Go
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

断言会发生Panic:

Go
1
Ω(ACTUAL).Should(Panic())
And/Or
Go
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无法满足需要,你可以实现接口:

Go
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

简化了外部进程的测试,可以:

  1. 编译Go二进制文件
  2. 启动外部进程
  3. 发送信号并等待外部进程退出
  4. 基于退出码进行断言
  5. 将输出流导入到gbytes.Buffer进行断言
gstruct

此包用于测试复杂的Go结构,提供了结构、切片、映射、指针相关的Matcher。

对所有字段进行断言:

Go
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"),
})

不处理某些字段: 

Go
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"), // 忽略多余字段
})

一个复杂的例子:

Go
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(),
}))

 

← Kubernetes端到端测试
LaTex语法速查 →
2 Comments On This Topic
  1. 回复
    绿色记忆:Go语言单元测试和仿冒
    2022/01/23

    […] 一个行为驱动测试框架,参考:Ginkgo学习笔记 […]

  2. 回复
    黄豆豆
    2023/02/17

    good note! thanks for sharing

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • Go语言单元测试和仿冒
  • 使用Mockito进行单元测试
  • Python单元测试
  • 基于Spring Test和Mockito进行单元测试
  • Go应用性能剖析

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2