Helm charts are notoriously tricky to test because they’re more than just YAML; they’re dynamic templates. helm-unittest aims to give you the confidence of unit testing without sacrificing the real-world fidelity of Helm’s templating engine.
Let’s see helm-unittest in action. Imagine you have a values.yaml like this:
replicaCount: 2
image:
repository: nginx
tag: latest
service:
type: ClusterIP
port: 80
And a templates/deployment.yaml that uses these values:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: nginx
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
A basic test file, templates/tests/deployment_test.yaml, would look like this:
suite: deployment
templates:
- deployment.yaml
tests:
- name: basic deployment
set:
replicaCount: 3
image:
tag: "1.21.0"
asserts:
- equal:
path: spec.replicas
value: 3
- equal:
path: spec.template.spec.containers[0].image
value: "nginx:1.21.0"
When you run helm-unittest --template-dir templates --values values.yaml templates/tests/deployment_test.yaml, it renders deployment.yaml with values.yaml and the set overrides, then checks if the asserts match the rendered output.
The problem helm-unittest solves is the "it works on my machine" syndrome for Kubernetes manifests generated by Helm. Helm’s Go templating is powerful but complex; subtle errors in logic, incorrect variable usage, or missing dependencies can lead to invalid or unexpected Kubernetes resources. Manually applying charts and inspecting output is slow and error-prone. helm-unittest brings deterministic, fast, and isolated testing to this process, allowing you to catch these issues before you deploy to a cluster.
Internally, helm-unittest works by simulating the Helm templating process. It takes your chart’s values.yaml, any provided test-specific set values, and the chart’s templates, then invokes Helm’s templating engine (the same one that runs helm install or helm template) to render the final Kubernetes manifests. It then uses a powerful assertion library to compare the rendered output against your expected state, checking for exact matches, presence of keys, or even complex JSON path evaluations.
The core levers you control are the set values and the asserts. The set block allows you to override your chart’s default values.yaml for a specific test case. This is crucial for testing different configurations, feature flags, or edge cases. The asserts block is where you define what you expect the rendered Kubernetes manifest to look like. You can use equal for exact string or numeric matches, contains to check if a string contains a substring, notExists to ensure a field isn’t present, and jsonpath for more intricate checks using JSONPath expressions.
You’re not limited to testing individual files. You can assert on multiple resources rendered from a single template file, or even test that a specific resource is not created under certain conditions. For instance, if a ConfigMap should only be created when a specific value is set, you can write a test that asserts its absence when that value is false and its presence when true.
A common pitfall is forgetting that Helm’s include function and named templates are also rendered. If your deployment.yaml uses {{ include "mychart.fullname" . }} and mychart.fullname is defined in a _helpers.tpl file, helm-unittest will render that _helpers.tpl as well. You don’t need to explicitly list _helpers.tpl in your templates: block if it’s included by another template. The test execution context includes all files in the templates directory.
The next concept you’ll likely grapple with is how to manage complex dependencies between resources, such as ensuring a Service references the correct Deployment’s selector.