logo
0
2
Login
docs: 增加 isNullOrEmpty & isEmpty 断言介绍

Helm 单元测试插件

无需部署 Kubernetes 集群,对 Helm Chart 进行单元测试。支持并发测试,仅对单个或部分文件进行测试(支持 glob 表达式匹配)。

镜像名

cnbcool/helm-unittest:latest

docker.cnb.cool/cnb/plugins/cnbcool/helm-unittest:latest

使用说明

main: push: ut: stages: - name: Execute Helm test cases image: cnbcool/helm-unittest:latest settings: mode: sequential helm_dir: my-helmchart/ tests_file: - misc/tests/job_test.yaml - misc/tests/client/*_test.yaml
  • mode: 执行模式, 该属性可以省略,默认为 sequential。支持下面的值。当填入一个不支持的值时,强制使用 sequential 模式。
    • sequential:顺序执行所有测试用例,并输出一个完整的测试报告。
    • concurrent:并发执行每一个测试用例,每一个测试用例会生成一个单独的测试报告,在所有用例并发执行完毕后,输出各个用例的测试报告。
    • split: 顺序执行所有测试用例,并分开输出每一个测试报告。
  • helm_dir:指定 helm chart 目录,该属性可以省略,默认为当前仓库。在多仓库项目中,可以指定项目下的 helm chart 目录来进行测试。
  • tests_file:指定要测试的用例文件(支持 glob 表达式匹配)。该属性可以省略,默认情况下将会执行 helm_dir/tests 目录下所有以 _test.yaml 结尾的测试文件。该属性常用在 helm chart 调试中,或仅执行某些关键的测试用例,避免整体执行测试,占用太长的时间。

测试文档开发规范

这个章节描述了如何在 YAML 文件中定义自己的单元测试用例。

测试套件

测试套件是通过单个文件定义的、具有相同目的和范围的测试用例集合。其根结构如下所示:

suite: 测试 Deploy Service

suite 属性的类型为字符串(string),且为可选属性,用于指定测试结果输出中显示的套件名称。例如:

PASS 测试 Deploy 和 Service tests/deploy_and_service_test.yaml

指定额外的 Values 文件

values 属性可指定额外的 values 文件,这些文件用于渲染 Helm Chart(类似于 helm install 命令的 -f--values 选项)。❗️文件路径需为测试套件文件所在目录的相对路径

values 属性是一个由字符串组成的数组类型,它也是一个可选属性。

例如,下面的测试套件指定了额外的 values 文件(values-override.yaml),它与当前测试文件位于相同目录下。那么意味着,./values-override.yaml 文件与 Helm Chart 中的 values.yaml 文件会共同渲染 Helm Chart,且 ./values-override.yaml 文件会覆盖 values.yaml 文件中相同的配置。

suite: 测试 Deployment values: - values-override.yaml

因此 values 属性中元素的顺序至关重要。下面的例子中,values 属性中包含了 4 个 YAML 文件。那么在整个渲染环节中,对于相同的配置内容,最后一个声明的文件会覆盖掉之前文件。

suite: 测试 Deployment values: - a.yaml - b.yaml - override/c.yaml - ../values-override.yaml

对于省略 values 属性的场景,只会使用 helm chart 中的 values.yaml 文件来渲染。

单独设置测试套件中的配置

set 属性用来单独设置测试套件中的一个或多个配置项。它是一个可省略的属性,值是一个对象(Object)类型。它的 key 是一个 values 配置路径,类似于 helm install 命令中的 --set 选项,例如 image.pullPolicy。该值可以是任意类型的数据(如字符串、数字、数组或对象),通过键的路径指定目标配置项。此设置会覆盖 values 文件中已存在的同名配置项

在下面的例子中,set 属性声明了一个配置属性路径 global.image.tag,且设定它的值为 latest。如果 values 中(Helm Chart 原本的 values.yaml 和 values 属性中定义的额外 values 文件)存在该配置项(即 global.image.tag),那么在执行测试前,set 会将这个配置项的值覆盖(也就是说,最终渲染时,global.image.tag 值为 latest)。若 values 中不存在该配置项,那么 set 也会将其添加到最终的配置项集合中。

suite: 我的测试套件名称 set: global.image.tag: latest

需要注意的是,set 的声明顺序与 values 无关。也就是说,下面两个例子中的书写顺序,不影响渲染逻辑。set 中定义的 global.image.tag 都会覆盖 values 文件中的值。

suite: 我的测试套件名称 set: global.image.tag: latest values: - ../values-override.yaml
suite: 我的测试套件名称 values: - ../values-override.yaml set: global.image.tag: latest

set 属性可以包含多个配置项路径:

suite: 我的测试套件名称 set: global.image.tag: latest group: enable: false mysql.args: - args1 - args2

💡一个好的最佳实践是使用额外的 values 文件(例如可以在 tests 目录下创建一个 additional_values 目录,用来存放特殊场景下的配置集合,tests/additional_values/suite1_values.yaml)包含配置项,从而避免在 set 中声明过多的配置。

suite: 我的测试套件名称 values: - additional_values/suite1_values.yaml

指定要测试的 YAML 文件

templates 属性是一个字符串数组(推荐使用)。该属性用于指定本次测试中需要渲染的模板文件范围,仅选中的文件会被实际渲染。模板文件存放在 templates 目录中,可通过 Linux 路径分隔符( /)指定其路径(注:templates/ 前缀省略)。通过使用通配符,无需逐个列举文件名即可批量测试多个模板。此外,部分模板(以下划线 _ 开头或扩展名为 .tpl 的模板)即使位于 templates 子目录中也会自动被纳入测试范围,无需手动添加。

例如,下面的测试套件用于测试 templates/redis-test/stateful_set.yamlltemplates/deployment.yaml 这两个文件:

suite: 测试 StatefulSet templates: - redis-test/stateful_set.yaml - deployment.yaml

定义 Release 对象

你可以使用 release 属性来定义 {{ .Release }} 对象。release 是一个可选属性,它拥有四个属性:

Release 名称

name 是一个可选的字符串属性,它用来定义 Release 名称,默认值是 "RELEASE-NAME"

在下面的例子中,测试套件声明一个 Release 名称为 cnb

suite: 测试 StatefulSet release: name: cnb

命名空间

namespace 属性用来表示 Release 所要部署的 Kubernetes 命名空间,默认值是 "NAMESPACE"。它是一个可选的字符串属性。

在下面的例子中,测试套件声明一个 Release 命名空间为 cnb

suite: 测试 StatefulSet release: namespace: cnb

修订次数

revision 属性表示当前 Release 的修订次数,默认值是 0。它是一个可选的整数属性。

💡 虽然 revision 应该是一个自然数,但是在测试用例中,声明成一个负数也不会报错。

在下面的例子中,测试套件声明当前的 Release 修订次数为 1:

suite: 测试 StatefulSet release: revision: 1

到底是更新还是安装?

release.upgrade 是一个可选的布尔属性。它代表当前的部署操作是更新还是安装。默认值为 false(表示安装)。

在下面的例子中,表示当前是一个更新操作:

suite: 测试 StatefulSet release: upgrade: true

定义 Capabilities 对象

可选的对象属性 capabilities 用于声明 {{ .Capabilities }} 对象。它拥有三个属性:

设置 Kubernetes 主版本号

capabilities.majorVersion 是一个可选的整型属性。它表示 Kubernetes 的主版本号(不是 Helm 版本),默认情况下,它的值由 Helm 版本来决定。

下面的示例设置 Kubernetes 的主版本号为 1,表示 v1.xxx

suite: 测试 StatefulSet capabilities: majorVersion: 1

设置 Kubernetes 次版本号

capabilities.minorVersion 是一个可选的整型属性。它表示 Kubernetes 的次版本号(不是 Helm 版本),默认情况下,它的值由 Helm 版本来决定。

下面的示例设置 Kubernetes 的次版本号为 23,表示 v1.23.0

suite: 测试 StatefulSet capabilities: majorVersion: 1 minorVersion: 23

💡 设置版本的场景很少,一个简单的场景是,Helm 代码根据当前的 Kubernetes 版本号采取不同的渲染行为。

例如下面的代码中,global.kube_version 在 Values 中的值为 "{{ .Capabilities.KubeVersion.Version }}"

apiVersion: v1 kind: ConfigMap metadata: name: environment-variables data: author: "{{ .Values.global.author }}" {{- $kube_version := tpl .Values.global.kube_version . }} kube_version: "{{ $kube_version }}" {{- if semverCompare ">=1.23.0" $kube_version }} support_grpc_in_probe: "true" {{- else }} support_grpc_in_probe: "false" {{- end }}

根据上面的代码,我们可以设计测试用例:

suite: 测试 config map capabilities: majorVersion: 1 minorVersion: 25 templates: - config_map.yaml tests: - it: Kubernetes version asserts: - equal: path: data.kube_version value: "v1.25.0" - equal: path: data.support_grpc_in_probe value: "true"

API 版本

属性 apiVersions 是一组版本集合,默认为定义的 kubernetes 版本所使用的版本集,为 Helm 安装本身的功能。

定义 Chart 对象

一个可选的 chart 对象属性用来定义 {{ .Chart }} 对象。 它有两个可选属性。

Chart 版本

使用可选的字符串属性 chart.version 来设置 Chart 的语义版本值。默认情况下,这个值由 Chart.yaml 定义。

例如在下面的例子中,Chart 版本号被设置为 1.3.0(即使 Chart.yaml 中定义的版本号可能是 1.1.0

suite: 测试 config map chart: version: 1.3.0

应用版本

使用可选的字符串属性 chart.appVersion 来设置 Chart 的应用版本号。默认情况下,这个值由 Chart.yaml 定义。

例如在下面的例子中,Chart 应用版本号被设置为 latest(即使 Chart.yaml 中定义的版本号可能是 1.0.0)。

suite: 测试 config map chart: appVersion: latest

跳过测试

一个可选的属性 skip 可以实现跳过当前测试套件。同时你必须使用 skip.reason 属性(字符串类型,且不可省略)去定义跳过的原因(😅否则的话,skip 将不会生效)。

例如下面的测试用例将会被跳过:

suite: 测试 ConfigMap templates: - templates/cm2.yaml release: namespace: default skip: reason: "WIP: 代码开发中"

后渲染

postRenderer 是一个可选的属性,它用于在 Helm 渲染之后,但在验证之前应用的操作。

📖 什么是后渲染

后渲染(post-render)是 Helm 3.1 引入的一个功能,允许你在 Helm 渲染模板之后、应用到 Kubernetes 之前,对渲染出来的 YAML 进行二次处理。它可以被用于 installupgrade,和 template 操作中。在 Helm 的使用场景中,通过命令行参数 --post-renderer 和一个可执行文件路径(例如脚本):

helm install mychart stable/wordpress --post-renderer /path/to/executable

简单来说:

  • Helm 渲染模板 → 得到 YAML
  • post-render 工具/脚本 → 处理 YAML(如注入、修改、过滤等)
  • 最终 YAML → 应用到集群

下面举个例子🌰

首先定义一个 ConfigMap templates/cm2.yaml

apiVersion: v1 kind: ConfigMap metadata: name: my-config data: key: "value1"

然后我们编写对应的测试文件 tests/cm2_test.yaml

suite: 测试 ConfigMap templates: - templates/cm2.yaml postRenderer: cmd: "yq" args: - "eval" - ".metadata.namespace=\"my-ns\"" tests: - it: should render ConfigMap when v1 is available asserts: - isKind: of: ConfigMap - equal: path: metadata.namespace value: my-ns

从上面的文件看出,我没有在 templates/cm2.yaml 中声明 metadata.namespace。但是在测试用例文件中,通过声明 postRenderer 属性,调用 yq 命令,将最终的渲染结果中添加了 metadata.namespace 信息,且它的值为 "my-ns"

通过上面的举例,你可以看到 postRenderer 拥有两个属性:

  1. cmd:字符串,必须存在的属性。要调用的命令的完整路径,或者如果它位于 $PATH 上,则仅显示其名称。
  2. args:由字符串组成的数组,表示命令行 cmd 的参数。

‼️测试作业

测试作业是测试的基本单位。每次运行测试作业时,都会渲染 Helm Chart,并使用测试中定义的断言进行验证。你可以使用外部 values 文件或直接在测试作业定义中设置用于在测试作业中呈现的 values。

属性 tests 表示一个数组,每一个元素都是一个作业结构。tests 是必须存在的属性,如果省略则会报错 no tests found.

suite: 测试 ConfigMap templates: - templates/cm2.yaml tests: - 作业 1 - 作业 2 - ...

为测试作业起一个名称

使用 TDD 风格或你喜欢的任何内容,在 tests 数组中的 it 属性中声明一个测试作业的名称。名字最好取一个有意义的名称,这样在维护的过程中,可以看出这个测试作业的主要通途。

例如下面的例子中,第一个测试作业的名字叫作元数据信息(这听起来像是在测试 metadata 下的内容是否符合预期)

suite: 测试 ConfigMap templates: - templates/cm2.yaml tests: - it: 元数据信息 ...

继承与覆盖

在 Helm 单元测试套件中,tests 是核心字段,用于声明具体的测试用例集合。值得注意的是,tests 块中支持使用许多与顶层(即测试文件最顶层)功能一致的属性(例如 valuessetrelease 等)。这些属性的设计遵循「继承-覆盖」规则,理解其行为对编写可靠的测试用例至关重要。

对于这些可继承的属性,它们的作用与最顶层直接声明时完全一致。不过需要注意的是,若在 tests 块中显式声明了与顶层同名的属性,则 tests 块内的配置会覆盖顶层的同名配置。这一机制确保了测试用例的隔离性——每个测试用例可独立调整配置,无需修改全局或顶层文件,避免测试间的相互干扰。

例如,在测试用例的顶层和 tests 块中配置如下:

suite: 覆盖测试 templates: - templates/cm2.yaml values: - values.yaml set: test.enable: false test.resources.limits.cpu: 28 tests: - it: 测试项目 set: test.enable: true values: - values2.yaml asserts: - hasDocuments: count: 1

此时,该测试用例的实际执行环境将为:

  • test.enabletrue ,而非顶层定义的 false
  • test.resources.limits.cpu 为 28
  • values 中的值为 [values2.yaml],而非顶层定义的 [values.yaml]

下面的列表中,汇总了所有可以被 tests 块继承的顶层配置属性:

  • values: 指定额外的 values 文件。 tests 块中的 values 属性仅对当前作业有效,再遇到重复的配置时,会覆盖 values 文件和外层的 values 配置(如果同时存在最外侧的 values
  • set: 单独设置测试作业中的配置。如果同时在 tests 块中和最外层配置了 set 属性,对于他们同时修改的配置,则只有测试作业的设置会生效(参考上方的示例)
  • release: 作用与顶层 release 属性相同。对于相同的 release 属性会进行覆盖(例如同时定义了 release.namespacetests 块中的声明会生效)
  • capabilities: 作用与顶层 capabilities 属性相同。对于相同的 capabilities 属性会进行覆盖(例如同时定义了 capabilities.majorVersiontests 块中的声明会生效)
  • chart: 作用与顶层 chart 属性相同。对于相同的 chart 属性会进行覆盖(例如同时定义了 chart.Versiontests 块中的声明会生效)
  • skip: 跳过当前测试作业,作用与顶层 skip 属性相同。
  • postRenderer: 后渲染。若同时在 tests 块中和顶层声明 postRenderer,则在当前测试作业中,以 tests 块中的为准(顶层的 postRenderer 不会触发执行)

下面的例子中,通过注释来标注哪些属性会生效,哪些不会:

suite: Test ConfigMap Rendering templates: - templates/cm2.yaml values: # 对 case1 不生效❌,对 case2 生效✅ - values1.yaml set: global.image.name: ut # 对 case1 不生效❌,对 case2 生效✅ global.image.tag: latest # 对 case1 生效✅,对 case2 也生效✅ release: name: ut # 对 case1 不生效❌,对 case2 生效✅ namespace: cnb # 对 case1 生效✅,对 case2 也生效✅ capabilities: majorVersion: 1 # 对 case1 生效✅,对 case2 也生效✅ minorVersion: 23 # 对 case1 不生效❌,对 case2 生效✅ chart: version: 1.2.0 # 对 case1 不生效❌,对 case2 生效✅ appVersion: latest # 对 case1 生效✅,对 case2 也生效✅ postRenderer: # 对 case1 不生效❌,对 case2 生效✅ cmd: yq args: - e - .metadata.namespace="yq" tests: - it: case1 values: - values-case1.yaml set: global.image.name: my-ut release: name: cnb capabilities: minorVersion: 28 chart: version: latest postRenderer: cmd: yq args: - e - .data.tool="yq" asserts: - equal: path: data.tool value: yq - it: case2 asserts: - equal: path: metadata.name value: my-config

文档索引选择器

有时候,Helm Chart 的开发者喜欢在一个 YAML 文件中使用 --- 分隔多个 Kubernetes 的定义声明。

例如下面的 YAML (templates/server.yaml) 中,定义了两个 Kubernetes 资源:Deployment 和 Service。

apiVersion: apps/v1 kind: Deployment metadata: name: auditor-server # ... --- apiVersion: v1 kind: Service metadata: name: auditor-service labels: app: auditor # ...

在 Helm 单元测试中,可以通过 documentIndex 属性来指定文档索引,即指定要测试的 YAML 文档。documentIndex 是一个可选的整数参数,默认值为 -1(即测试 YAML 文件中声明的最后一个文档)。

下面我们编写两个单元测试用例,用来测试上述 YAML 文件中的 Deployment 和 Service:

suite: "测试 templates/server.yaml" templates: - templates/cm2.yaml tests: - it: 测试 Deployment documentIndex: 0 asserts: - equal: path: metadata.name value: auditor-server - it: 测试 Service documentIndex: 1 asserts: - equal: path: metadata.name value: auditor-service

文档选择器

除了上个章节中介绍的 documentIndex(按索引的方式选择 YAML 文档),一个最佳实践是使用 documentSelector 来选择文档。documentSelector 是一个可选参数,它是一个对象类型。它可以在众多 YAML 文档中,通过匹配关键的 key-value 来选择你要检查的内容。

还是对于上面的例子(templates/server.yaml):

apiVersion: apps/v1 kind: Deployment metadata: name: auditor-server # ... --- apiVersion: v1 kind: Service metadata: name: auditor-service labels: app: auditor # ...

我们使用 documentSelector 来代替 documentIndex

suite: "测试 templates/server.yaml" templates: - templates/server.yaml tests: - it: 测试 Deployment documentSelector: path: metadata.name value: auditor-server asserts: - equal: path: metadata.name value: auditor-server - it: 测试 Service documentSelector: path: metadata.name value: auditor-service asserts: - equal: path: metadata.name value: auditor-service

也就是说,在第一个测试用例“测试 Deployment” 中,documentSelector 通过匹配 metadata.name = auditor-server 来选择要检查的内容。

允许匹配多个文档

如果 documentSelector 通过 key-value 的形式匹配了多个文档怎么办?

例如我们对原有的 templates/server.yaml 进行改造,增加了一个 ReplicationController 资源,且它的名称与 Deployment 一致:

apiVersion: apps/v1 kind: Deployment metadata: name: auditor-server namespace: cnb # ... --- apiVersion: v1 kind: ReplicationController metadata: name: auditor-server namespace: builder # ... --- apiVersion: v1 kind: Service metadata: name: auditor-service labels: app: auditor # ...

那么运行之前的测试用例,结果会报错:

FAIL 测试 templates/server.yaml tests/cm2_test.yaml - 测试 Deployment - asserts[0] `equal` fail Error: multiple indexes found

这是因为测试用例中,documentSelector 通过 metadata.name 选择文档。而上述 YAML 中出现了两个相同的 metadata.name 资源(即一个 Deployment 和一个 ReplicationController)。

我们可以通过可选的参数 matchMany 来允许文档选择器匹配多个文档。matchMany 是一个布尔类型的参数,默认值为 false

如果允许多文档匹配,那么上面的错误就会消失。不过在这种情况下,断言只会检查匹配到的随机一个文档。因此这个开关不建议打开。

suite: "测试 templates/server.yaml" templates: - templates/server.yaml tests: - it: 测试 Deployment documentSelector: path: metadata.name value: auditor-server # 💡因为会匹配到多个文档,因此为了避免报错,matchMany 须为 true。 # 但是下面的断言只会检查匹配到的随机一个文档 matchMany: true asserts: - equal: path: metadata.name value: auditor-server - equal: path: metadata.namespace value: cnb - it: 测试 Service documentSelector: path: metadata.name value: auditor-service asserts: - equal: path: metadata.name value: auditor-service

跳过空文档检查

在 Helm Chart 渲染时,有些模板(如通过 {{- if ... }} 控制的)在某些 values 下可能不会生成任何资源(即渲染结果为空)。在 helm-unittest 的测试中,如果你指定了 documentSelector 来选择某些模板文档进行断言,默认情况下,即使该模板渲染为空(没有生成任何文档),测试也会尝试对其断言,可能导致测试失败。

我们再次修改下 templates/server.yaml 中的内容,这次 Deployment 是否生成将会由一个开关 .global.server.enable 来控制:

{{- if .Values.global.server.enable }} apiVersion: apps/v1 kind: Deployment metadata: name: auditor-server namespace: cnb # ... {{- end }} --- apiVersion: v1 kind: Service metadata: name: auditor-service labels: app: auditor # ...

在测试用例中,我们强行关闭 .global.server.enable,来模拟开关关闭的情况。如果注释掉 documentSelector.skipEmptyTemplates,那么用例会报错 document not found

suite: "测试 templates/server.yaml" templates: - templates/server.yaml tests: - it: 测试 Deployment set: global.server.enable: false documentSelector: path: metadata.name value: auditor-server #skipEmptyTemplates: true asserts: - equal: path: metadata.name value: auditor-server - it: 测试 Service documentSelector: path: metadata.name value: auditor-service asserts: - equal: path: metadata.name value: auditor-service

💡建议不要在测试中开启 documentSelector.skipEmptyTemplates。因为测试用例的用途就是为了检查 helm chart 中的组合逻辑。

‼️断言

断言是判断渲染结果是否符合预期的属性,它在测试作业中定义,属性名称为 assertsasserts 是一个必须存在的数组属性,每一个元素都是一个断言对象。

每一个断言对象的格式为:

# ... tests: - it: 测试作业名称 asserts: - <断言类型>: # 断言类型参数(不同的断言类型所包含的参数可能不尽相同) <断言对象参数> (固定的一组可选参数)

下面的章节将会逐一介绍 ⬇️

[断言类型] 检查指定文档是否存在

containsDocument 是一个断言类型,它用来检查是否有指定的 kindapiVersion 文档被渲染。它有下面几个参数:

  • kind: 一个必须存在的字符串类型参数。它表示预期存在的文档所属于的 Kubernetes 资源类型,例如 Deployment
  • apiVersion: 一个必须存在的字符串类型参数。它表示预期存在的文档所属于的 Kubernetes API 版本,例如 apps/v1
  • name: 一个可选的字符串类型参数。它表示预期的 metadata.name 值(即资源名称)
  • namespace: 一个可选的字符串类型参数。它表示预期的 metadata.namespace 值(即所属的 Kubernetes 命名空间)

例如,下面的断言例子中,用来断言资源类型是一个 Deployment,且它的 Kubernetes API 是 apps/v1。该 Deployment 名称为 foo,创建后位于 bar 命名空间下。

# ... tests: - it: containsDocument 示例 asserts: - containsDocument: kind: Deployment apiVersion: apps/v1 name: foo namespace: bar

[断言类型] 断言数组是是否包含指定的元素

contains 是一个断言类型,它用来断言数组内是否包含指定的元素。它有下面几个参数:

  • path: 一个必须存在的字符串类型参数。表示所要断言的数组 YAML 路径。
  • content: 一个必须存在的任意类型参数(可以是字符串,也可以是整数、布尔值、对象等)。它表示要包含的内容。
  • count: 一个可选的整数参数。表示预期匹配内容的次数。
  • any: 一个可选的布尔参数,默认为 false。当 any 值为 true 时,contains 不会强制要求 content 中的所有内容都必须满足,只需要部分存在即可。

例如,下面的例子中,用来断言渲染后的容器环境变量列表,是否包含 LOG_IEVEL, 值为 info,有且只有定义了一次(如果在 container 中定义了两次环境变量,count 检测就会报错):

# ... tests: - it: contains 示例 asserts: - contains: path: spec.template.spec.containers[0].env content: name: LOG_IEVEL value: info count: 1

如果我们只关心环境变量 LOG_LEVEL 是否存在,且只被定义了一次,而不关心它的值(即部分匹配),那么可以使用可选的 any 参数,并将其设置为 true

# ... tests: - it: contains 示例 asserts: - contains: path: spec.template.spec.containers[0].env content: name: LOG_IEVEL count: 1 any: true

[断言类型] 断言不包含某个元素

notContains 用来断言渲染后的某个属性下,不包含某项元素。

‼️需要注意的是,notContains 可以作用于任何类型,而 contains 只能断言数组类型。

下面介绍 notContains 参数:

  • path: 一个必须存在的字符串类型参数。表示所要断言的 YAML 路径
  • content: 一个可选的任意类型参数(可以是字符串,也可以是整数、布尔值、对象等)。它表示不包含的内容。当 content 省略时,notContains 只会用来断言当前 YAML 路径是否存在(若不存在则断言成功)
  • any: 一个可选的布尔参数,默认为 false。当 any 值为 true 时,notContains 不会强制要求 content 中的所有内容都必须不存在,只需要部分不存在即可。

下面的例子用来断言渲染后某个 YAML 路径不存在:

# ... tests: - it: 不存在 spec.template.spec.affinity.nodeAffinity asserts: - notContains: path: spec.template.spec.affinity.nodeAffinity

💡 在判断某个 YAML 路径不存在的场景中,虽然 notContains 可以准确有效判断,但一个好的最佳实践是使用 notExists。参考下面的章节 “断言 YAML 路径是否存在”

第二个例子用来判断 Service 的端口转发信息中,不包含特定的信息:

# ... tests: - it: Service 端口转发中不存在 unknown asserts: - notContains: path: spec.ports content: name: unknown port: 8888

如果仅仅只想验证 Service 的端口转发信息中,只包含 port: 8888,而不关心其他属性,可以将 any 设置为 true

# ... tests: - it: Service 端口转发中不存在 unknown asserts: - notContains: path: spec.ports content: # name: unknown port: 8888 any: true

[断言类型] 断言相等

equal 类型用来断言某个 YAML 路径下的值是否与预期结果相同。

介绍一下 equal 参数:

  • path: 一个必须存在的字符串类型参数。表示所要断言的 YAML 路径
  • value: 一个可选的任意类型参数。表示预期的值。当 value 省略时,表示预期为 null。
  • decodeBase64: 一个可选的布尔值。当它为 true 时,会让断言在比较前,自动把实际值 base64 解码,再和期望值对比。

下面的例子中,会用来判断 Secret 的类型是否是预期的 Opaque,以及它的注解(annotation)是否为 "helm.sh/resource-policy": "keep"

# ... tests: - it: secret 类型 asserts: - equal: path: type value: Opaque - it: secret 注解 asserts: - equal: path: metadata.annotations value: "helm.sh/resource-policy": "keep"

decodeBase64 常用于 Secret 测试。例如,下面的 Secret 内容如下:

apiVersion: v1 kind: Secret metadata: name: my-secret annotations: "helm.sh/resource-policy": "keep" type: Opaque data: PWD: "{{ .Values.global.pwd | b64enc }}"

那么测试用例可以这么写(假设 .global.pwd 的值为 "abc"):

# ... tests: - it: 测试 Secret asserts: - equal: path: data.PWD value: "abc" decodeBase64: true

[断言类型] 断言不相等

notEqual 用来断言某个 YAML 路径下的值是否与预期结果不相同。它的参数如下:

  • path: 一个必须存在的字符串类型参数。表示所要断言的 YAML 路径
  • value: 一个可选的任意类型参数。表示预期不相等的值。当 value 省略时,表示预期为 null。
  • decodeBase64: 一个可选的布尔值。当它为 true 时,会让断言在比较前,自动把实际值 base64 解码,再和期望值对比。

notEqualequal 用法接近,这里不再额外举例。

[断言类型] 断言非结构化文本

有两个断言类型 equalRawnotEqualRaw,用来断言非结构化文本(例如 txt 文件)。它们只有一个共同的参数 value(不可省略的字符串参数),用来将渲染后的结果与 value 的内容进行比较。

下面的例子中,用来判断 templates/NOTES.txt 中的内容是否为 hello world,而不是 Nothing

suite: "测试 NOTES" templates: - NOTES.txt tests: - it: 测试 NOTES asserts: - equalRaw: value: "hello world" - notEqualRaw: value: "Nothing"

[断言类型] 断言 YAML 路径是否存在

有两个断言类型 existsnotExists,用来判断渲染后的 YAML 路径是否存在。他们只有一个共同的参数 path(不可省略的字符串参数,表示断言的 YAML 路径)。

下面的例子中,我们断言 ConfigMap 中是存在 metadata.name ,且不存在 metadata.namespace

# ... tests: - it: 测试 ConfigMap asserts: - exists: path: metadata.name - notExists: path: metadata.namespace

💡 提示:

  1. ❗️强烈反对使用 isNotNull 来代替 exists
  2. ❗️同理,反对使用 isNull 来代替 notExists
  3. 一个最佳实践是使用 notExists 来断言一个路径不存在,而不是使用 notContains

[断言类型] 断言错误信息

在编写 Helm 代码的过程中,常常使用 fail 函数来报错退出,并在控制台上打印错误内容(有点类似于 Go 语言中的 panic 函数或是 Python 中的异常)。

举个🌰。下面的代码是一个 ConfigMap。它根据 values.yaml 中定义的数组来循环创建:

{{- range $index, $node := .Values.nodes }} apiVersion: v1 kind: ConfigMap metadata: name: environment-variables-{{ $node.id }} data: # ... --- {{- end }}

上面的代码存在一个缺陷:当 values 中定义的 nodes 下存在重复的 id 时,渲染会生成若干具有相同名称的 ConfigMap。于是我们使用 fail 在渲染前检查 id 是否重复。若重复,则报错退出(整个 Helm Chart 不会被创建):

{{- $ids := dict }} {{- range $index, $node := .Values.nodes }} {{- $id := $node.id | toString }} {{- if hasKey $ids $id }} {{- fail (printf "Duplicate node id found: %s" $id) }} {{- else }} {{- $_ := set $ids $id true }} {{- end }} {{- end }} {{- range $index, $node := .Values.nodes }} apiVersion: v1 kind: ConfigMap metadata: name: environment-variables-{{ $node.id }} data: # ... --- {{- end }}

在上述场景中,为了检查代码逻辑是否正确生效,我们可以在单元测试中使用 failedTemplate 来断言报错信息。它拥有两个参数:

  • errorMessage:一个可选的字符串参数。它用值表示 fail 函数产生的错误消息。如果不一致,则断言失败
  • errorPattern:一个可选的字符串参数。它使用一个正则匹配值,用来匹配 fail 函数产生的错误消息。若匹配成功,则表示断言成功。正则匹配规则可以参考 Go 正则匹配

根据上面场景的 ConfigMap 代码,我们来编写测试用例。

# ... tests: - it: 重复的 id set: nodes: - id: 1 - id: 1 asserts: - failedTemplate: errorMessage: "Duplicate node id found: 1" - failedTemplate: errorPattern: "^Duplicate node id found: (.*)"

如果同时省略 errorMessageerrorPattern,表示没有错误消息产生。

# ... tests: - it: 不重复的 id set: nodes: - id: 1 - id: 2 asserts: - failedTemplate: {}

💡 虽然 failedTemplate: {} 表示没有错误消息产生,但一个语义化更强的方式是使用 notFailedTemplate: {} 来替代。

[断言类型] 比较大小

Helm 单元测试提供了 6 种比较大小的断言类型,他们是:

  • greaterOrEqual:大于等于。
  • notGreaterOrEqual:小于。
  • equal:相等。
  • notEqual:不相等。
  • lessOrEqual:小于等于。
  • notLessOrEqual:大于。

💡提示:equalnotEqual 用法可以参考 断言相等断言不相等 章节,这里不再赘述。

greaterOrEqualnotGreaterOrEquallessOrEqualnotLessOrEqual 拥有相同的参数:

  • path:要断言的路径(string)。
  • value:比较的大小。支持数字和字符串类型。

例如,我们断言 Deploymentspec.replicas 属性:

# ... tests: - it: replica asserts: - greaterOrEqual: path: spec.replicas value: 2

需要注意的是,虽然 greaterOrEqualnotGreaterOrEquallessOrEqualnotLessOrEqual 拥有的 value 参数支持字符串类型,但不推荐这么使用。使用字符串的场景是,要断言的某个 path 是字符串类型(如果是其它类型则会报错),且它是一个可以转换成数字(整数或浮点数)的字符串类型。如果 path 指向的值不是一个可转换为数字的字符串,那么 Helm 单元测试不会报错

[断言类型] 文档数量

有些时候,开发者喜欢将一组相关的 Kubernetes 资源写在一个 YAML 文件里。并用 --- 将它们隔开。例如:

{{- if .Values.nginx.enabled }} {{/* 这里定义一个 Deployment */}} apiVersion: apps/v1 kind: Deployment metadata: name: my-nginx {{/* 后续内容省略 */}} --- {{/* 这里在定义一个 Service */}} apiVersion: v1 kind: Service metadata: name: my-service {{/* 后续内容省略 */}} {{- end }}

如果开关 .Values.nginx.enabled 打开,那么 Deployment 和 Service 必须同时创建,否则同时不存在。不允许他们之间单独创建。这时我们可以使用 hasDocuments 来断言该场景。

hasDocuments 用来断言渲染过程中产生的文档数量(例如上个例子中开关开启后,会渲染出 2 个文档)。默认情况下,测试套件中设置的 documentIndexdocumentSelector 不会对该断言类型生效。 hasDocuments 支持以下两个参数:

  • count: 整数类型。表示预期渲染的文档数。
  • filterAware:可选的布尔参数。当它为 true 时,测试套件中设置的 documentIndexdocumentSelector 会对 hasDocuments 生效(默认为 false)。

因此,经过刚刚的介绍,我们可以设计下面的测试用例,来检查上面的代码:

# ... tests: - it: document case 1 set: nginx.enabled: true asserts: - hasDocuments: count: 2 - it: document case 2 documentIndex: 0 asserts: - hasDocuments: count: 1 filterAware: true - it: document case 3 documentSelector: path: metadata.name value: my-service asserts: - hasDocuments: count: 1 filterAware: true

[断言类型] API 版本

如果要断言一个 Kubernetes 对象的 API 版本,可以使用下面的测试:

# ... tests: - it: API Version asserts: - equal: path: apiVersion value: v2

一个更加方便的方式是使用 isAPIVersion 断言类型,它拥有一个必选的字符串参数 of,表示预期的 API 版本。例如:

# ... tests: - it: API Version asserts: - isAPIVersion: of: v2

尽管使用 isAPIVersion 来断言 API 版本是一个好的选择,但更推荐使用 containsDocument,因为它不仅可以断言 API 版本,还可以拥有更多的功能。详细内容请参考 [断言类型] 检查指定文档是否存在

[断言类型] 清单类型

在 Kubernetes 清单中,使用 Kind 属性定义其所属类型。在 Helm 单元测试中,isKind 用来断言清单类型,它有一个字符串参数 of ,表示预期的清单类型:

# ... tests: - it: Manifest kind asserts: - isKind: of: Deployment

isAPIVersion 类似,在实际编写 Helm 单元测试中,更推荐使用 containsDocument,因为它不仅可以断言清单类型,还可以拥有更多的功能。详细内容请参考 [断言类型] 检查指定文档是否存在

[断言类型] 判断值是否为空

断言指定路径的值为空或为 null(即值为 null""0[]{})。该断言类型可以使用 isNullOrEmptyisEmpty。只有一个字符串参数 path,表示要断言的 YAML 路径。

# ... tests: - it: null or empty asserts: - isNullOrEmpty: path: spec.clusterIPs - it: empty asserts: - isEmpty: path: spec.my

需要注意的是,无法使用 isNullOrEmptyisEmpty 断言一个路径不存在。对于一个不存在的路径,isNullOrEmptyisEmpty 会报错 unknown path 并失败。