目录

Ginkgo 学习

1 概述

ginkgo 是一个 BDD 框架,Kubernetes 的 E2E 测试使用该框架实现集群的测试。

ginkgo 是集成在 Go 测试框架的,在目录下执行 ginkgo bootstrap 就会构建测试的入口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package ginkgo_test

import (
	"testing"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

func TestGinkgo(t *testing.T) {
    // Gomega 连接 Ginkgo
    // 使得 Gomega 断言能够通知到 Ginkgo
	RegisterFailHandler(Fail)

    // 测试入口
	RunSpecs(t, "Ginkgo Suite")
}

接着,我们在同目录下可以创建测试 Spec,然后通过 ginkgogo test 命令就可以执行测试。

2 构建 Spec

2.1 Describe Context It

为了更好的构建测试的结构,ginkgo 提供了三个接口来构建测试项的结构:

  • Describe: 定义一个测试项
  • Context: 定义一个测试项下的一种情况
  • It: 真正执行的测试代码

以下面为例,Describe 定义了顶层的测试项与其中一个方面的测试项,两个 Context 为两个测试的情况,而 It 包含了具体的测试代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Root Describe
var _ = Describe("Book", func() {
	// Sub Describe
	Describe("Categorizing book length", func() {

		// Context1
		Context("With more than 300 pages", func() {
			It("should be a novel", func() {
				// 测试代码 + 断言
			})
		})

		// Context2
		Context("With fewer than 300 pages", func() {
			It("should be a short story", func() {
				// 测试代码 + 断言
			})
		})

	})

})
Note
也可以简单的将 Describe 与 Context 理解为将测试分类,复用一些描述情况。

2.2 BeforeEach AfterEach

BeforeEach 会在每个 It 执行之前执行,一般用于测试进行数据的初始化。

 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
// Root Describe
var _ = Describe("Book", func() {
    var (
        // 通过闭包在 BeforeEach 和 It 之间共享数据
        longBook  Book
        shortBook Book
    )

    BeforeEach(func() {
        longBook = Book{
            Title:  "Les Miserables",
            Author: "Victor Hugo",
            Pages:  1488,
        }
 
        shortBook = Book{
            Title:  "Fox In Socks",
            Author: "Dr. Seuss",
            Pages:  24,
        }
    })

	// Sub Describe
	Describe("Categorizing book length", func() {

		// Context1
		Context("With more than 300 pages", func() {
			It("should be a novel", func() {
				// 测试代码 + 断言
			})
		})

		// Context2
		Context("With fewer than 300 pages", func() {
			It("should be a short story", func() {
				// 测试代码 + 断言
			})
		})

	})
})

相反,AfterEach 会在每个 It 执行完成后执行,用于数据的销毁。

2.3 JustBeforeEach JustAfterEach

JustBeforeEach 会在每个 It 执行之前执行,在对应的 BeforeEach 之后执行。

JustBeforeEach 出现主要是为了抽象出初始化数据的逻辑,使得只需要 BeforeEach 提供初始化数据来源。

例如下面例子,过 JustBeforeEach 就可以将原来的两次 BeforeEach 中的 NewBookFromJSON 调用,减少为一次。

 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
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
                }`
            })
        })
    })
})

对应的,JustAfterEach 在每个 It 之后调用,在 AfterEach 之前调用。

2.4 BeforeSuite AfterSuite

BeforeSuite 会在所有的测试执行前执行,AfterSuite 在所有的测试执行后执行:

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

2.5 SynchronizedBeforeSuite SynchronizedAfterSuite

SynchronizedBeforeSuite 用于指定在主进程执行的函数,以及在各个子进程执行的函数。函数都在运行测试之前执行。

Note

如果没有通过 "–nodes " 指定进程数量,那么默认就是一个主进程,一个子进程执行测试。

因此,SynchronizedBeforeSuite/SynchronizedAfterSuite 类似于 BeforeSuite/AfterSuite 会被执行。

例如,下面示例在子进程创建前,运行创建数据库。在各个子进程运行测试前,执行创建 client。

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

2.6 By

By 函数用于添加一些块文档,如果测试失败时会打印出来。可以将其理解为错误日志。

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

3 Spec Runner

3.1 Pending Spec

定义一个 Spec 或容器时,调用 P/X 前缀的接口,会定义一个 Pending Spec。默认不会执行

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

通过命令行参数 –noisyPendings=false 可以将其执行。

3.2 Skiping Spec

通过 Skip 接口,你可以在代码中运行时跳过某个 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")
    }
})

或者,你可以通过 –skip= 跳过运行匹配的 Spec。

3.3 Focused Specs

通过 F 前缀的接口,可以默认仅仅执行 Focused Spec

1
2
3
FDescribe("some behavior", func() { ... })
FContext("some scenario", func() { ... })
FIt("some assertion", func() { ... })

或者,你可以在执行命令时通过 –focus= 指定运行 Spec。

3.3 Parallel Specs

默认下,Ginkgo 执行测试是串行的,你可以通过 ginkgo -p 开启并行测试,Ginkgo 会自动创建适当数量的进程来并行执行。通过 ginkgo -nodes=N 也可以执行进程数量。

多个 go test 子进程会消费队列中的 Spec 并行执行。

4 Gomega

Gomega 库提供了断言的功能,

4.1 断言

Ω 与 Expect 都提供断言的功能,完全相同。

通过 Should/To 来进行 “应该” 逻辑的断言,通过 ShouldNot/NotTO/ToNot 进行 “不应该” 逻辑的断言。

1
2
3
4
5
6
Expect(ACTUAL).Should(Equal(EXPECTED))
Expect(ACTUAL).To(Equal(EXPECTED))

Expect(ACTUAL).ShouldNot(Equal(EXPECTED))
Expect(ACTUAL).ToNot(Equal(EXPECTED))
Expect(ACTUAL).NoTo(Equal(EXPECTED))

Should 等函数后跟着 Matcher interface 变量,代表一个表达式。

4.2 Matcher

4.2.1 判断类型与值相等

  • Equal 使用 reflect.DeepEqual 进行比较。
  • BeEquivalentTo 会先将 ACTUAL 转换为 EXPECTED 的类型,然后使用 reflect.DeepEqual 进行比较。
1
2
Expect(ACTUAL).Should(Equal(EXPECTED))
Expect(ACTUAL).Should(BeEquivalentTo(EXPECTED))

4.2.2 接口相容

  • BeAssignableToTypeOf 仅仅用于判断能否将 EXPECTED 赋值给 ACTUAL。常用于判断 interface 是否满足。

4.2.3 空值与零值

  • BeNil 判断是否为 nil
  • BeZero 判断是否为零值
1
2
Expect(ACTUAL).Should(BeNil())
Expect(ACTUAL).Should(BeZero())

4.2.4 布尔值

  • BeTrue 判断为 true
  • BeFalse 判断为 false
1
2
Expect(ACTUAL).Should(BeTrue())
Expect(ACTUAL).Should(BeFalse())

4.2.5 error 处理

  • HaveOccurred 判断 error 为 nil
  • Succeed 判断 error 不为 nil
  • MatchError 以判断 error 或者 string 是否相同
1
2
3
4
Expect(err).ShouldNot(HaveOccurred())
Expect(err).Should(Succeed())
Expect(err).Should(MatchError("an error")) //asserts that err.Error() == "an error"
Expect(err).Should(MatchError(SomeError)) //asserts that err == SomeError (via reflect.DeepEqual)

4.2.6 channel 处理

  • BeClosed 判断 channel 已经关闭,会读取 channel 数据来判断
  • Receive 判断 channel 中能否读取到数据
  • BeSent 判断 channel 能否无阻塞发送消息
1
2
3
Expect(ch).Should(BeClosed())
Expect(ch).Should(Receive(<optionalPointer>))
Expect(ch).Should(BeSent(VALUE))

4.2.7 文件处理

  • BeAnExistingFile 判断文件/目录是否存在
  • BeARegularFile 判断是否为普通文件
  • BeADirectory 判断是否为目录
1
2
3
Expect(ACTUAL).Should(BeAnExistingFile())
Expect(ACTUAL).Should(BeARegularFile())
Expect(ACTUAL).Should(BeADirectory())

4.2.8 字符串处理

  • ContainSubstring 判断是否包含子串
  • HavePrefix 判断是否包含前缀
  • HaveSuffix 判断是否包含后缀
  • MatchRegexp 进行正则匹配
1
2
3
4
Expect(ACTUAL).Should(ContainSubstring(STRING, ARGS...))
Expect(ACTUAL).Should(HavePrefix(STRING, ARGS...))
Expect(ACTUAL).Should(HaveSuffix(STRING, ARGS...))
Expect(ACTUAL).Should(MatchRegexp(STRING, ARGS...))

4.2.9 JSON/XML/YAML 处理

  • MatchJSON 判断 JSON 是否相同
  • MatchXML 判断 XML 是否相同
  • MatchYAML 判断 YAML 是否相同

4.2.10 集合 string array map chan slice 处理

  • BeEmpty 判断为空
  • HaveLen 判断长度
  • HaveCap 判断容量
  • ContainElement 判断是否包含元素
  • BeElementOf 判断值是否在集合其中一个
  • ConsistOf 判断两个集合元素是否相同,不考虑顺序
  • HaveKey 判断 map 是否包含指定的 key
  • HaveKeyWithValue 判断 map 是否包含指定的 key/value
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ints := []int{1, 2, 3}
m := map[string]string{}
Expect(ints).Should(BeEmpty())
Expect(ints).Should(HaveLen(1))
Expect(ints).Should(HaveCap(2))
Expect(ints).Should(ContainElement(3))
Expect(1).Should(BeElementOf(ints)
Expect(ints).Should(ConsistOf(1, 2, 3))
Expect(ints).Should(ConsistOf([]int{3, 2, 1}))
Expect(m).Should(HaveKey("a"))
Expect(m).Should(HaveKeyWithValue("a", "b"))

4.2.11 数字/时间处理

  • BeNumerically 进行数值的比较,包含以下运算符:
    • == 判断数值是否相等
    • ~ 判断数值是否相似(一定范围内)
    • > >= < <= 比较大小
  • BeBetween 判断是否在范围内
  • BeTemporally 进行 time.Time 类型比较,方式与 BeNumerically 相同
1
2
3
4
5
6
7
8
9
a := 1
b := 2
Expect(a).Should(BeNumerically("==", b))
Expect(a).Should(BeNumerically("~", b, 1))
Expect(a).Should(BeNumerically(">", b))
Expect(a).Should(BeNumerically(">=", b))
Expect(a).Should(BeNumerically("<", b))
Expect(a).Should(BeNumerically("<=", b))
Expect(a).Should(BeBetween(0, 10))

4.2.12 panic

  • Panic 判断函数是否会发生 panic
1
Expect(func(){}).Should(Panic())

4.2.13 逻辑组合

  • SatisfyAll/And 进行逻辑与的组合
  • SatisfyAny/Or 进行逻辑或的组合
1
2
Expect(msg).To(And(Equal("Success"), MatchRegexp(`^Error .+$`)))
Expect(msg).To(Or(Equal("Success"), MatchRegexp(`^Error .+$`)))