24.7 Chart Tests: Validating a Release Works
Right, so you’ve built a Helm chart. It looks beautiful. The templates are elegant, the values.yaml is a model of clarity. But let’s be honest: you have absolutely no idea if it will actually work when someone runs helm install. Pushing a broken chart is the Helm equivalent of showing up to a black-tie event with your fly down. It’s unprofessional, and everyone will see it. This is where chart tests come in—they’re your automated fly-check.
Think of these tests less like a formal QA suite and more like a smoke test. Their job isn’t to validate your application’s business logic; that’s what your application’s own test suite is for. Their job is to answer one critical question: “Does this chart, with these values, produce a set of Kubernetes manifests that the cluster can actually run without immediately vomiting an error?”
The Anatomy of a Test
A chart test is just another template file living in the templates/ directory, but it’s blessed with a special annotation that tells Helm, “Hey, don’t actually install this with the rest of the stuff; I’m for testing.” These files are conventionally named templates/tests/ and use the test- prefix, but the annotation is what truly matters.
Here’s a classic example: a simple test for a web server chart that ensures the service is actually routing traffic correctly. It’s a Pod definition that runs a container which curls the service.
# templates/tests/test-service-connectivity.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-service-test"
annotations:
"helm.sh/hook": test
"helm.sh/hook-weight": "-5" # We'll get to weights in a second
spec:
containers:
- name: curl-container
image: curlimages/curl:latest # A tiny, excellent image for this job
command: ['curl']
args: ['--silent', '--show-error', '--fail', 'http://{{ .Release.Name }}-my-web-service:8080/health'] # Assumes a health endpoint
restartPolicy: Never
The magic sauce is the helm.sh/hook: test annotation. This tells the Helm client, “Execute this Pod as part of the helm test command.” When you run helm test <RELEASE_NAME>, Helm will look for all these annotated resources, create them, and then wait for them to succeed. For a Pod, success means its containers exit with a status code of 0. If they exit with anything else, the test fails.
The Delicate Art of Test Dependencies
Here’s the first “questionable choice” you’ll run into: Helm doesn’t inherently know the order of operations for your tests. If you have multiple test pods, they’ll all be launched at once by default. This is a problem if one test depends on another resource being ready first.
This is where the helm.sh/hook-weight annotation comes in. It’s a bit of a clunky solution, honestly. You assign each test hook a numerical weight, and Helm sorts them ascending (i.e., -5 runs before -4, which runs before 0, which runs before 5). Negative numbers are your friend for tests that need to run before the default weight of 0.
Let’s say you have a test that checks your database migration job ran and a separate test that checks the API that depends on that database. You’d weight them accordingly:
# templates/tests/test-migrations.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-migrations-test"
annotations:
"helm.sh/hook": test
"helm.sh/hook-weight": "-10" # Run first
spec:
template:
spec:
containers:
- name: migrate
image: my-app-image:latest
command: ['/bin/migrate-db', '--verify']
restartPolicy: Never
---
# templates/tests/test-api-connectivity.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-api-test"
annotations:
"helm.sh/hook": test
"helm.sh/hook-weight": "0" # Run after the migrations (weight -10)
spec:
containers:
- name: curl-api
image: curlimages/curl:latest
command: ['curl']
args: ['http://{{ .Release.Name }}-api-service:8080/api/v1/status']
Best Practices and Pitfalls
Keep Tests Lean and Mean: Your test image should be as small as possible.
curlimages/curl(~10MB) is a fantastic choice over something like the fullubuntuimage (~70MB). This isn’t the place for your full application container. You just need a tool to poke at what you’ve deployed.Always Set
restartPolicy: Never: This is non-negotiable. If the test container fails, you want it to fail. ArestartPolicy: OnFailurewould cause the Pod to keep restarting, making Helm’s test runner wait until it hits the timeout, which is a frustratingly slow way to discover a simple failure.Cleanup is Your Job: This is the biggest rough edge. When a test Pod completes, it just sits there in
Completedstate. Helm does not delete it for you. You’ll have to runhelm test --cleanupto delete these test resources. If you don’t, your namespace will slowly fill up with hundreds of completed test pods. It’s a bizarre omission, so just get in the habit of always using the--cleanupflag:helm test <RELEASE> --cleanup.Test Different Values: Don’t just test your default
values.yaml. The real value is in testing edge cases. Runhelm installwith a minimal value set (--set resources.requests.memory=64Mi) and then runhelm testto see if your application actually starts with constrained resources. This can catch overly optimistic defaults before your users do.
Ultimately, chart tests are a form of defensive coding for your infrastructure. They’re not glamorous, but writing them is what separates a chart that might work from one you can deploy with genuine confidence.