无需部署 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
sequential。支持下面的值。当填入一个不支持的值时,强制使用 sequential 模式。
sequential:顺序执行所有测试用例,并输出一个完整的测试报告。concurrent:并发执行每一个测试用例,每一个测试用例会生成一个单独的测试报告,在所有用例并发执行完毕后,输出各个用例的测试报告。split: 顺序执行所有测试用例,并分开输出每一个测试报告。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 文件,这些文件用于渲染 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
templates 属性是一个字符串数组(推荐使用)。该属性用于指定本次测试中需要渲染的模板文件范围,仅选中的文件会被实际渲染。模板文件存放在 templates 目录中,可通过 Linux 路径分隔符( /)指定其路径(注:templates/ 前缀省略)。通过使用通配符,无需逐个列举文件名即可批量测试多个模板。此外,部分模板(以下划线 _ 开头或扩展名为 .tpl 的模板)即使位于 templates 子目录中也会自动被纳入测试范围,无需手动添加。
例如,下面的测试套件用于测试 templates/redis-test/stateful_set.yamll 和 templates/deployment.yaml 这两个文件:
suite: 测试 StatefulSet
templates:
- redis-test/stateful_set.yaml
- deployment.yaml
你可以使用 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.majorVersion 是一个可选的整型属性。它表示 Kubernetes 的主版本号(不是 Helm 版本),默认情况下,它的值由 Helm 版本来决定。
下面的示例设置 Kubernetes 的主版本号为 1,表示 v1.xxx:
suite: 测试 StatefulSet
capabilities:
majorVersion: 1
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"
属性 apiVersions 是一组版本集合,默认为定义的 kubernetes 版本所使用的版本集,为 Helm 安装本身的功能。
一个可选的 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 进行二次处理。它可以被用于
install,upgrade,和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 拥有两个属性:
cmd:字符串,必须存在的属性。要调用的命令的完整路径,或者如果它位于 $PATH 上,则仅显示其名称。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 块中支持使用许多与顶层(即测试文件最顶层)功能一致的属性(例如 values、set、release 等)。这些属性的设计遵循「继承-覆盖」规则,理解其行为对编写可靠的测试用例至关重要。
对于这些可继承的属性,它们的作用与最顶层直接声明时完全一致。不过需要注意的是,若在 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.enable 为 true ,而非顶层定义的 falsetest.resources.limits.cpu 为 28[values2.yaml],而非顶层定义的 [values.yaml]下面的列表中,汇总了所有可以被 tests 块继承的顶层配置属性:
tests 块中的 values 属性仅对当前作业有效,再遇到重复的配置时,会覆盖 values 文件和外层的 values 配置(如果同时存在最外侧的 values)tests 块中和最外层配置了 set 属性,对于他们同时修改的配置,则只有测试作业的设置会生效(参考上方的示例)release 属性相同。对于相同的 release 属性会进行覆盖(例如同时定义了 release.namespace, tests 块中的声明会生效)capabilities 属性相同。对于相同的 capabilities 属性会进行覆盖(例如同时定义了 capabilities.majorVersion,tests 块中的声明会生效)chart 属性相同。对于相同的 chart 属性会进行覆盖(例如同时定义了 chart.Version,tests 块中的声明会生效)skip 属性相同。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 中的组合逻辑。
断言是判断渲染结果是否符合预期的属性,它在测试作业中定义,属性名称为 asserts。asserts 是一个必须存在的数组属性,每一个元素都是一个断言对象。
每一个断言对象的格式为:
# ...
tests:
- it: 测试作业名称
asserts:
- <断言类型>:
# 断言类型参数(不同的断言类型所包含的参数可能不尽相同)
<断言对象参数> (固定的一组可选参数)
下面的章节将会逐一介绍 ⬇️
containsDocument 是一个断言类型,它用来检查是否有指定的 kind 和 apiVersion 文档被渲染。它有下面几个参数:
metadata.name 值(即资源名称)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 是一个断言类型,它用来断言数组内是否包含指定的元素。它有下面几个参数:
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 参数:
notContains 只会用来断言当前 YAML 路径是否存在(若不存在则断言成功)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 参数:
下面的例子中,会用来判断 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 路径下的值是否与预期结果不相同。它的参数如下:
notEqual与equal用法接近,这里不再额外举例。
有两个断言类型 equalRaw 和 notEqualRaw,用来断言非结构化文本(例如 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"
有两个断言类型 exists 和 notExists,用来判断渲染后的 YAML 路径是否存在。他们只有一个共同的参数 path(不可省略的字符串参数,表示断言的 YAML 路径)。
下面的例子中,我们断言 ConfigMap 中是存在 metadata.name ,且不存在 metadata.namespace:
# ...
tests:
- it: 测试 ConfigMap
asserts:
- exists:
path: metadata.name
- notExists:
path: metadata.namespace
💡 提示:
- ❗️强烈反对使用
isNotNull来代替exists- ❗️同理,反对使用
isNull来代替notExists- 一个最佳实践是使用
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 来断言报错信息。它拥有两个参数:
fail 函数产生的错误消息。如果不一致,则断言失败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: (.*)"
如果同时省略 errorMessage 和 errorPattern,表示没有错误消息产生。
# ...
tests:
- it: 不重复的 id
set:
nodes:
- id: 1
- id: 2
asserts:
- failedTemplate: {}
💡 虽然
failedTemplate: {}表示没有错误消息产生,但一个语义化更强的方式是使用notFailedTemplate: {}来替代。
Helm 单元测试提供了 6 种比较大小的断言类型,他们是:
greaterOrEqual:大于等于。notGreaterOrEqual:小于。equal:相等。notEqual:不相等。lessOrEqual:小于等于。notLessOrEqual:大于。💡提示:
equal和notEqual用法可以参考 断言相等 和 断言不相等 章节,这里不再赘述。
greaterOrEqual,notGreaterOrEqual,lessOrEqual,notLessOrEqual 拥有相同的参数:
path:要断言的路径(string)。value:比较的大小。支持数字和字符串类型。例如,我们断言 Deployment 的 spec.replicas 属性:
# ...
tests:
- it: replica
asserts:
- greaterOrEqual:
path: spec.replicas
value: 2
需要注意的是,虽然 greaterOrEqual,notGreaterOrEqual,lessOrEqual,notLessOrEqual 拥有的 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 个文档)。默认情况下,测试套件中设置的 documentIndex 和 documentSelector 不会对该断言类型生效。 hasDocuments 支持以下两个参数:
count: 整数类型。表示预期渲染的文档数。filterAware:可选的布尔参数。当它为 true 时,测试套件中设置的 documentIndex 和 documentSelector 会对 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
如果要断言一个 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,[],{})。该断言类型可以使用 isNullOrEmpty 或 isEmpty。只有一个字符串参数 path,表示要断言的 YAML 路径。
# ...
tests:
- it: null or empty
asserts:
- isNullOrEmpty:
path: spec.clusterIPs
- it: empty
asserts:
- isEmpty:
path: spec.my
需要注意的是,无法使用 isNullOrEmpty 或 isEmpty 断言一个路径不存在。对于一个不存在的路径,isNullOrEmpty 或 isEmpty 会报错 unknown path 并失败。