End to End (e2e) 测试模拟用户行为操作 Kubernetes,用于保证 Kubernetes 服务或集群的行为完全符合设计。
E2E Framework
如何工作
查看 Kubernetes 源码即可了解如何使用 E2E 框架,入口点:
1 | func TestE2E(t *testing.T) { |
可以看到这是一个标准的Go Test,它调用 e2e.go 中定义的:
1 | import ( |
上述代码主要就是启动测试套件。实际执行Spec之前会调用下面的函数启动进行准备工作:
1 | func setupSuite() { |
setupSuite执行完毕之后,Ginkgo会运行子目录中的数千个Specs。
编写 spec
Kubernetes项目中有大量例子可以参考,例如:
1 | package e2e |
测试分类
可以为E2E测试添加标签,以区分类别:
标签 | 说明 |
---|---|
无 | 测试可以快速(5m以内)完成,支持并行测试,具有一致性 |
[Slow] | 执行比较慢的用例 |
[Serial] | 不支持并发的测试用例,比如占用太多资源,还比如需要重启Node的 |
[Disruptive] | 可能影响(例如重启组件、Taint节点)不是该测试自己创建的工作负载。任何Disruptive测试自动是Serial的 |
[Flaky] | 标记测试中的问题难以短期修复。这种测试默认情况下不会运行,除非使用focus/skip参数 |
[Feature:.+] | 如果一个测试运行/处理非核心功能,因此需要排除出标准测试套件,使用此标签。 |
[LinuxOnly] | 需要使用Linux特有的特性 |
此外,任何测试都必须归属于某个SIG,并具有对应的 [sig-<name>]
标签。每个e2e的子包都在framework.go中SIGDescribe函数,来添加此标签:
1 | // SIGDescribe annotates the test with the SIG label. |
当然除了以上标签,还有个比较重要的标签就是 [Conformance],
此标签用于验收Kubernetes集群最小功能集。所以如果你有个私有部署的k8s集群,就可以通过这套用例来搞验收。方法也很简单,通过下面几步就可以执行:
1 | # under kubernetes folder, compile test cases and ginkgo tool |
注意,kubernetes的测试使用的镜像都放在GCR上了,如果你的集群在国内,且还不带FQ功能,那可能会发现pod会因为下载不了镜像而启动失败。
E2E Utils
此库是定义在 kubernetes/test/e2e/framework/util.go 中的一系列常量、变量、函数:
- 常量:各种操作的超时值、集群节点数量、CPU剖析采样间隔
- 变量:各种常用镜像的URL,例如BusyBox
- 函数:
- 命名空间操控:
- CreateTestingNS:创建一个新的,供当前测试使用的命名空间
- DeleteNamespaces:删除命名空间
- CheckTestingNSDeletedExcept:检查所有e2e测试创建的命名空间出于Terminating状态,并且阻塞直到删除
- Cleanup:读取文件中的清单,从指定命名空间中删除它们,并检查命名空间中匹配指定selector的资源正确停止
- 节点操控:
- 设置Label、设置Taint
- RemoveLabelOffNode、RemoveTaintOffNode 删除Label/Taint
- AllNodesReady:检查所有节点是否就绪
- GetMasterHost:获取哦Master的主机名
- NodeHasTaint:判断节点是否具有Taint
- Pod操控:
- CreateEmptyFileOnPod:在Pod中创建文件
- LookForStringInPodExec:在Pod中执行命令并搜索输出
- LookForStringInLog:搜索Pod日志
- WaitForAllNodesSchedulabl:e等待节点可调度
- 日志和调试:
- CoreDump:登陆所有节点并保存日志到指定目录
- DumpDebugInfo:输出测试的调试信息
- DumpNodeDebugInfo:输出节点的调试信息
- 网络操控:
- BlockNetwork:通过操控Iptables阻塞两个节点之间的网络
- UnblockNetwork:解除阻塞
- 控制平面操控:
- RestartApiserver:重启API Server
- RestartKubelet:重启Kubelet
- 断言,若干Expect、Fail函数
- 收集CPU、内存等剖析信息Gather
- 执行Kubectl命令:KubectlCmd
- 创建K8S客户端:
- LoadConfig:加载K8S配置
- LoadClientset:创建客户端对象
- Ginkgo API封装:
- KubeDescribe
- 等待各种资源达到某种状态:Wait
- 其它杂项:
- OpenWebSocketForURL:打开WebSocket连接
- PrettyPrintJSON:格式化JSON
- Run:运行命令
- 命名空间操控:
其文档位于 https://godoc.org/k8s.io/kubernetes/test/e2e/framework 。
实战
下载 k8s 代码,并checkout到指定分支,这里以 release-1.18 为例
1 | git clone https://github.com/kubernetes/kubernetes.git |
build e2e测试 manifest
1 | hack/generate-bindata.sh |
生成 e2e.test 二进制
1 | cd test/e2e && GOOS=linux GOARCH=amd64 go test -c -o e2e.test -v |
登陆到待测试k8s集群节点,上传e2e.test到指定节点,配置e2e.test需要的镜像仓库列表
1 | export KUBE_TEST_REPO_LIST=/home/ubuntu/e2e/repos.yaml //导出环境变量 |
执行e2e测试,这里以networkpolicy为例
1 | ./e2e.test -ginkgo.focus=NetworkPolicy --disable-log-dump --provider="skeleton" --kubeconfig="/root/.kube/config" > >(tee e2e.log) |
也可以使用 ginkgo
1 | ./ginkgo --focus="NetworkPolicy" ./e2e.test -- --disable-log-dump --provider="skeleton" --kubeconfig="/root/.kube/config" > >(tee e2e.log) |
总结
研究Kubernetes的 e2e 测试框架,然后类比我们以往的经验,下面几点特性还是值得借鉴的:
All e2e compiled into one binary
在对服务端程序进行API测试时,我们经常会针对每个服务都创建一个ginkgo suite来框定测试用例的范围,这样做的好处是用例目标非常清晰,但是随着服务数量的增多,这样的suite会越来越来多。从组织上,看起来就稍显杂乱,而且不利于测试服务的输出。
比如,我们考虑这么一个场景,QA需要对新机房部署,或者私有机房进行服务验证。这时候,就通常需要copy所有代码到指定集群在运行了,非常的不方便,而且也容易造成代码泄露。
kubernetes显然也会有这个需求,所以他们改变写法,将所有的测试用例都编译进一个 e2e.test
的二进制,这样针对上面场景时,就可以直接使用这个可执行文件来操作,非常的方便。
当然可执行文件的方便少不了外部参数的自由注入,以及整体测试用例的精心标记。否则,测试代码写的不规范,需要频繁的针对特定环境修改,也是特别不方便的。
Each case has a uniqe namespace
为每条测试用例创建一个独立的空间,是kubernetes e2e framework的一大精华。每条测试用例独享一个空间,彼此不冲突,从而根本上避免并发困扰,借助ginkgo的CLI来运行,会极大的提高执行效率。
而且这处代码的方式也非常优美,很有借鉴价值:
1 | func NewFramework(baseName string, options FrameworkOptions, client clientset.Interface) *Framework { |
利用 ginkgo 的 BeforeEach 的嵌套特定,虽然在Describe下就定义framework的初始化(如下),但是在每个 It 执行前,上面的BeforeEach才会真正执行,所以并不会有冲突:
1 | var _ = framework.KubeDescribe("GKE local SSD [Feature:GKELocalSSD]", func() { |
当然 e2e 框架还负责case执行完的环境清理,并且是按需灵活配置。比如你希望,case失败保留现场,不删除namespace,那么就可以设置flag 参数 delete-namespace-on-failure
为false来实现。
Asynchronous wait
几乎所有的Kubernetes操作都是异步的,所以不管是产品代码还是测试用例,都广泛的使用了这个异步等待库:kubernetes/vendor/k8s.io/apimachinery/pkg/util/wait
。这个库,实现简单,精悍,非常值得学习。
另外,针对测试的异步验证,其实ginkgo(gomega)本身提供的Eventualy,也是非常好用的。
Suitable logs
Kubernetes e2e 主要使用两种方式输出log,一个是使用glog库,另一个则是 framework.Logf 方法。glog本身是golang官方提供的log库,使用比较灵活。但是这里主要推荐的还是Framework.Logf。因为使用此方法的log会输出到GinkgoWriter里面,这样当我们使用ginkgo.RunSpecsWithDefaultAndCustomReporters方法时,log不光输出到控制台,也会保存在junit格式的xml文件里,非常方便在jenkins里展示测试结果。
Clean code
我们从Kubernetes e2e能看到很多好的借鉴,比如:
- 抽取主干方法,以突出测试用例主体
- 采用数据驱动方式书写共性测试用例
- 注释工整,多少适宜
- 不输出低级别log
- 代码行长短适宜
- 方法名定义清晰,可读性强
参考资料
- Kubernetes End-to-end Testing for Everyone
- KEP: NetworkPolicy verification rearchitecture
- https://github.com/kubernetes/kubernetes/blob/master/test/e2e/network/network_policy.go
- https://blog.csdn.net/shida_csdn/article/details/107198325
- https://events19.linuxfoundation.org/wp-content/uploads/2017/11/Verify-Your-Kubernetes-Clusters-with-Upstream-e2e-Tests-Kenichi-Omichi-NEC.pdf
- https://jimmysong.io/kubernetes-handbook/develop/testing.html
- https://blog.gmem.cc/kubernetes-e2e-test