yangxt65535

社区测试体系与自动化

背景

社区测试现状

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

pyramid

社区术语对应:

  • 集成、端到端测试
  • 功能测试
  • 单元测试
类型单元测试功能测试集成、端到端测试
测试对象函数、结构体单个模块的接口、服务完整特性系统
依赖策略全部由 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 库的文件读写进行接口的抽象。

    Afero github主页

  • 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),便于测试脚本编写。
  • 丰富的测试工具和库:测试框架 & 各类请求、数据库操作等库,便于模拟。
  • 功能测试相对简单,对性能要求低。

Pytest指南

集成测试

在集成测试中,测试用例应当分为两类:

  • 后端集成,在后端进行资源的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上的高性能执行。

Playwright指南

前后端混合?

在存量测试用例中,的确存在同时包含前端和后端的操作的用例,建议进行拆分。在新特性的集成测试用例设计过程中,应当确认测试对象的范围,尽可能避免前后端操作的深度耦合。

代码放哪里?

  • 功能测试: 在模块的仓库中,创建 tests/function 路径,存放该模块的功能测试用例代码,用分支或标签跟踪版本。
  • 集成测试: 在独立的 e2e 测试仓库中,按特性、版本存放于 tests/integration 或 e2e 目录。