社区测试体系与自动化
背景
社区测试现状
测试金字塔是一种自动化测试过程的金字塔形策略结构,用来指导软件开发过程中,各层自动化测试的投入比例,分别是UI测试、服务/接口测试、单元测试,越接近金字塔底部的测试活动,投入的工作量应该越多。

社区术语对应:
- 集成、端到端测试
- 功能测试
- 单元测试
| 类型 | 单元测试 | 功能测试 | 集成、端到端测试 |
|---|---|---|---|
| 测试对象 | 函数、结构体 | 单个模块的接口、服务 | 完整特性系统 |
| 依赖策略 | 全部由 Mock 提供 | 轻量真实依赖(本地实例、容器) | 全量真实依赖(集群内服务、CRD) |
| 运行环境 | go test 进程内 | 本地环境、容器镜像、 | 完整生产-like 集群 |
| 运行时长 | 毫秒级 | 秒级 | 分钟级 |
现状:缺少功能测试——开发人员只完成单元测试,将后续的交给测试;测试人员默认各模块功能正常,直接设计集成测试用例。因此需要推动测试左移,由开发者主导功能测试用例的设计与实现。
导向
开发者需要以模块为粒度,完成功能测试用例的设计与实现。测试人员则聚焦集成测试,避免职责交叉。功能测试用例在全部测试用例中应占主要比例。
功能测试用例的设计与模块规格紧密相关,建议在特性设计阶段就完成用例设计。集成测试用例则可以在开发基本完成后、转测前,由测试人员主导设计与实现。
在开发 UT、功能测试过程中发现的问题,可作为开发自提单,并在特性问题单中占据合理占比。
flowchart LR
A[特性设计(功能测试用例)] --> B[功能开发]
B --> C[集成测试用例设计]
B --> D[单元、功能测试实现]
C --> E{转测}
D --> E
E --> F[集成测试]
F --> G[专项测试]
功能测试不依赖集群内服务,对运行环境需求在门禁可实现的范围内。可与门禁集成,作为代码合入质量保障的一部分。
目标:确保开发者在转测时的特性质量保障,降低打回率;减少测试团队多次回归测试需要,释放人力到性能、安全、长稳等专项测试。
功能测试
Golang
测试框架上,使用 Ginkgo + Gomega 的组合
Ginkgo是Golang生态的成熟测试框架之一,构建于Go内置的testing库之上,并结合了Gomega断言库,提供了行为驱动开发风格的测试体验。K8s、etcd、Harbor 等社区都以之作为测试框架都在用。
- 提供 Describe / Context / It 的分层 DSL,不用依赖函数名描述测试用例
- 更强大的 Before/After 生命周期管理,保障用例之间的环境复用与隔离
- 并发支持更强。
- 断言的自动轮训与超时处理,降低代码复杂性。
- 报告输出更直观,层级、失败点和 stack 信息非常明确。
// 参数化测试用例描述
ginkgo.It("adds two numbers", func() {
sum := 1 + 2
gomega.Expect(sum).To(gomega.Equal(3))
})
// 分层结构化
var _ = ginkgo.Describe("Calculator", func() {
ginkgo.Describe("Add", func() {
ginkgo.Context("positive numbers", func() {
ginkgo.It("adds correctly", func() {
gomega.Expect(Add(2, 3)).To(gomega.Equal(5))
})
})
ginkgo.Context("mixed sign numbers", func() {
ginkgo.It("handles negative values", func() {
gomega.Expect(Add(-1, 3)).To(gomega.Equal(2))
})
})
})
})
var _ = ginkgo.BeforeSuite(func() {
// 全局初始化
counter = 100
})
var _ = ginkgo.AfterSuite(func() {
// 全局清理
})
var _ = ginkgo.Describe("Counter suite", func() {
ginkgo.BeforeEach(func() {
// 每个 It 开始前执行
counter++
})
ginkgo.AfterEach(func() {
// 每个 It 结束后执行
})
ginkgo.It("increments counter first time", func() {
gomega.Expect(counter).To(gomega.Equal(101))
})
ginkgo.It("increments counter second time", func() {
gomega.Expect(counter).To(gomega.Equal(102))
})
})
各类 Mock 工具:
文件系统 Mock:
Go 原生 os 包提供的 CreateTemp、MkdirTemp 等,可在真实文件系统中创建临时文件或目录。适合快速模拟文件操作,但需要手动清理,且不适合高并发场景。
It("File system mock using os.CreateTemp", func() { tmpFile, _ := os.CreateTemp("", "example-*.txt") defer os.Remove(tmpFile.Name()) n, _ = tmpFile.WriteString("hello world") Expect(n).To(Equal(11)) data, _ := os.ReadFile(tmpFile.Name()) Expect(string(data)).To(Equal("hello world")) })相比之下,使用 afero 文件系统库提供了虚拟文件系统以及抽象文件系统接口,允许在测试中创建独立虚拟磁盘环境,降低对测试环境的影响。但是可能需要改造源码,对原有基于原生 os 库的文件读写进行接口的抽象。
HTTP 服务 Mock:
原生 net/http/httptest 库可创建完整的 TestServer,模拟 RESTful 或 RPC 接口,支持请求捕获和响应定制,便于单元或集成测试。
由于 TestServer 只接受一个 httpHandler,如果有多路由需求,可以与 http.ServeMux 配合使用
It("HTTP mock using httptest", func() { mux := http.NewServeMux() mux.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "foo response") }) mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "bar response") }) ts := httptest.NewServer(mux) defer ts.Close() resp, _ := http.Get(ts.URL + "/foo") body, _ := ioutil.ReadAll(resp.Body) resp.Body.Close() Expect(string(body)).To(ContainSubstring("foo response")) })Kubernetes 集群 Mock:
envtest(controller-runtime 提供)可启动轻量级 apiserver 和 etcd,大部分 Kubernetes 行为可被模拟,实现对 CRD、控制器逻辑的功能测试,而无需真实集群。
It("K8s mock using envtest", func() { testEnv := &ctrl.Environment{} cfg, _ := testEnv.Start() defer testEnv.Stop() Expect(cfg).ToNot(BeNil()) // 用 mock config 创建 k8s client k8sClient, _ := ctrl.NewClient(cfg, ctrl.Options{}) // ... })其他依赖实例 Mock:
Testcontainers 基于 Docker 容器管理依赖服务(数据库、缓存、消息队列等),提供自动化创建、启动和销毁,保证测试环境隔离且接近生产环境。
It("Container mock using Testcontainers", func() { ctx := context.Background() req := testcontainers.ContainerRequest{ Image: "redis:7", ExposedPorts: []string{"6379/tcp"}, WaitingFor: wait.ForListeningPort("6379/tcp"), } redisC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) defer redisC.Terminate(ctx) port, _ := redisC.MappedPort(ctx, "6379") Expect(port.Int()).To(BeNumerically(">", 0)) })
Python
对于非 Golang 开发的项目(如C++、Python等),推荐使用 Python Pytest 作为测试框架。不考虑项目内部接口、模块的调用时,Python 作为脚本语言更适合测试脚本的开发。
- 编码逻辑(控制流、函数调用、面向对象)与主流语言一致(相比JavaScript),便于测试脚本编写。
- 丰富的测试工具和库:测试框架 & 各类请求、数据库操作等库,便于模拟。
- 功能测试相对简单,对性能要求低。
集成测试
在集成测试中,测试用例应当分为两类:
- 后端集成,在后端进行资源的CRUD操作,并查看相关工作负载或配置文件的变更是否按预期
- 前端集成/UI,以用户视角在浏览器前端页面操作,并查看前端的交互结果是否按预期
后端集成
框架的选型参考 Kubernetes 与 OpenShift 社区,对于复杂集成测试,同样使用 Ginkgo 测试框架,并在一定程度上复用 Kubernetes 社区的 e2e/framework 测试框架。在社区特性测试用例的开发过程中,将常用操作或对象抽取为测试公用包。
K8s 集成测试规模大、并行度高、逻辑复杂,社区基于 Golang 生态的常用测试框架 Ginkgo 与断言库 gomega 开发用例。对于复杂的集成测试,社区在此基础上封装了一个专用测试框架
e2e/framework模块,提供以下功能:
- 测试上下文与集群配置,封装了和测试环境、集群配置有关的全局信息。
- 命名空间与资源生命周期管理,包括测试前后的创建与清理。
- 基于 Gomega 的断言与错误处理封装,提供标准化错误信息输出。
- 常用对象操作 Helper,如 e2epod 提供 Pod 管理、自动等待等,e2enode 提供节点调度、污点管理功能。
- 日志与诊断收集
- kubectl等外部工具封装,减少远程调用命令的代码复杂性
前端集成/UI
对于涉及前端操作的测试(无论集成与否),则需要引入前端自动化工具,已实现打开网页、点击按钮、输入文本、截屏等交互流程的自动化。涉及前端的用例使用与开发一致的 JavaScript/TypeScript 语言,允许前端开发者快速上手测试用例的开发。
建议使用 Playwright(JavaScript版本)作为前端场景下的测试框架。功能更强、设计更现代、更具技术前景,且生态也在快速成熟,能够为社区质量保障提供更强支撑。相比 Cypress, Playwright 主要优势如下:
- 语义化定位增强测试代码的稳定性,降低维护成本;
- 广泛的浏览器支持保障项目在不同平台上的一致性;
- 并发+多上下文隔离能力,允许在CI/CD上的高性能执行。
前后端混合?
在存量测试用例中,的确存在同时包含前端和后端的操作的用例,建议进行拆分。在新特性的集成测试用例设计过程中,应当确认测试对象的范围,尽可能避免前后端操作的深度耦合。
代码放哪里?
- 功能测试: 在模块的仓库中,创建 tests/function 路径,存放该模块的功能测试用例代码,用分支或标签跟踪版本。
- 集成测试: 在独立的 e2e 测试仓库中,按特性、版本存放于 tests/integration 或 e2e 目录。