本篇博客将分析k8s e2e(端到端测试的)流程,从而实际的说明ginkgo的使用方法

Ginkgo测试框架

ginkgo作为k8s本身的测试框架对gotest进行了进一步的封装。支持测试用例的并行,串行,充实,过滤等等机制。下面简单介绍一下ginkgo的相关概念。

核心概念

  • suite: 一整个测试工程,测试工程包含很多测试用例(spec)
  • spec: 一个测试用例,通常表现为
    1
    2
    It("description", func{
    实际的测试逻辑})
  • container node: 组织spec的层级系统,spec类似于文件而container node类似于文件夹。具体有如下几类。当然这些container node在用法上没什么不同,只是名字不一样。用来提示在测试过程走到了那个分支
    • Describe
    • Context
    • When
  • setup node:在实际执行spec/suite之前或者之后的操作过程,具体有如下几类:
    • BeforeEach
    • JustBeforeEach
    • BeforeSuite
    • AfterEach
    • JustAfterEach
    • AfterSuite
  • subject nodes:具体的spec测试用例

下面是一个典型的包含各种node和spec的样例

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
func TestExample(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Example Suite")
}

var _ = BeforeSuite(func() {
fmt.Println("== BeforeSuite: 测试套件开始前执行一次 ==")
})

var _ = AfterSuite(func() {
fmt.Println("== AfterSuite: 测试套件结束后执行一次 ==")
})

var _ = Describe("计算功能", func() { // container node
BeforeEach(func() {
fmt.Println(" -> BeforeEach: 每个Spec前执行")
})

JustBeforeEach(func() {
fmt.Println(" -> JustBeforeEach: Spec前最后一步")
})

AfterEach(func() {
fmt.Println(" -> AfterEach: 每个Spec后执行")
})

JustAfterEach(func() {
fmt.Println(" -> JustAfterEach: Spec后立即执行")
})

Context("加法运算", func() { // container node
When("两个正数相加", func() { // container node
It("结果应该为正数", func() { // spec
fmt.Println(" * Running Spec: 正数 + 正数")
Expect(1 + 2).To(Equal(3))
})
})
})

Context("减法运算", func() {
It("结果可以为负数", func() {
fmt.Println(" * Running Spec: 小数 - 大数")
Expect(2 - 5).To(Equal(-3))
})
})
})

建树和运行阶段

gingkgo将执行测试分为两个阶段,构建树(决定节点的执行顺序,可以理解为编译)以及实际运行节点。下面是两阶段的具体说明

  • Tree Construction Phase(构建树阶段)
    • Ginkgo 会解析所有 Describe、Context、When、It 等声明,注册所有节点,把它们组织成一棵测试树
    • 该阶段发生于go test运行时,实际执行测试之前,在这个阶段,不会执行实际的测试逻辑,只是建立测试结构
  • Run Phase (运行阶段)
    • Ginkgo假设所有spec是独立的,这是并行运行测试,随机运行测试的基础
    • 由于spec是独立的,故而当spec测试抛出错误时则立即中止,执行其他的spec
    • Ginkgo使用修饰符(如Ordered、Serial,Flake,Filtering,Timeout等)来进行spec运行的串行化,重试,过滤,中断与超时机制的实现,例如如下的样例实现了再并行spec运行过程中某一进程串行运行spec的过程

K8s gingko framework

K8s e2e 对ginkgo做出一次封装,写了自己的framework。该framework拓展了如下的功能

  • 针对每个测试spec(此部分由framework维护)
    • 自动管理对k8s集群的链接(k8s e2e是使用集群外部来控制集群的测试过程,所以要对目标k8s集群进行链接)
    • 创建并回收命名空间(譬如测试schedule的相关功能,则会创建一个k8s-e2e-schedule[名称可能不准确]的ns,在这个ns中进行测试)
  • 针对整个e2e测试套件suite(此部分由testcontext维护)
    • 重定向k8log至GinkgoWriter,整合错误日志
    • 设置 Gomega 的默认超时和轮询间隔
    • 检测集群配置并生成Beartoken
    • 设置云提供商配置
    • 设置报告目录和 并输出Json 报告
  • 针对k8s各个模块做了测试方法的封装(譬如创建pod,volume,进行网络联通测试等)。

代码在这里

下面将以代码中schedule这一个组件(或者说功能)来进行framework的解释。先来看看这个文件夹中都测了点啥吧

  • predicate测试:本地临时存储资源限制测试,Pod 开销计算测试,NodeAffinity 不匹配测试,NodeAffinity 匹配测试,NodeAffinity 匹配测试,Taints-Tolerations 不匹配测试,HostPort 冲突测试,Pod 拓扑分布测试,SchedulingGates 测试
  • priority测试:Pod 反亲和性调度测试,Pod 容忍度优先级测试,Pod 拓扑分布评分测试
  • preemption测试: Pod 中断条件验证测试,Pod 拓扑分布抢占测试,抢占执行路径测试,PriorityClass 端点测试
  • limit_range测试:LimitRange 默认值应用测试,LimitRange 集合操作测试
  • nvdia-gpu测试:基础 NVIDIA GPU 设备插件测试,GPU 设备插件节点重建测试

下面介绍一下framework是怎么拉起测试的

  • 在framework中解析命令行参数,大部分ginkgo 测试都会以ginkgo test -flags来进行,这些flags掌控了测试的细节
  • 根据这些命令行参数初始化testcontext,testcontext维护整个e2e测试框架的具体参数
  • 进入reportBeforeSuite节点,该节点指定并初始化report模块的功能
  • 主测试函数所在文件通过导入schedule包从而自动注册schdule包下的container和context节点
  • 针对每测试,这里就是指上述的predicate、priorty…的测试。都初始化framework,设置并创建namespace,连接目标集群
  • 执行测试点
  • 测试点执行完毕,执行reportAfterEach节点,收集信息
  • framework将namespace 清理并删除
  • 执行reportAfterSuite节点,输出测试结果

K8s e2e 测试流程

在上面的介绍中我们知道了k8s是如何执行e2e 测试的。但我们还需要关心k8s是如何发起e2e测试,以及测试完成之后的种种处理。

简单来说,在提交pr至k8s仓库时会自动触发e2e测试流程

具体的流程如下

  • 提交 RR 到 kubernetes
  • Prow 监听到 PR webhook
  • Prow 触发 pre-submit Job(如 pull-kubernetes-e2e-kind
  • Job 执行测试脚本:
    • 在目标集群中创建 Pod,在 Pod 内使用 DIND 启动 kind 集群
    • 执行 e2e-k8s.sh 脚本,该脚本完成编译 e2e.test,启动 kind 集群,执行 E2E 测试
  • 测试开始执行:ginkgo-e2e.sh
    • 使用 Ginkgo 运行 test/e2e/**.go 中的测试用例
    • 示例测试包括 DNS、Pod 创建、Deployment 滚动更新等
  • job测试通过,等待approver和reviewer审批后由Tide合并;失败则通过 bot 注释反馈 GitHub。