How do you test Helm charts using helm test and chart-testing (ct)?
Quick Answer
Helm test runs test hook pods defined inside a chart to validate a deployed release in-cluster, while chart-testing (ct) is a CLI tool from the Helm community that automates linting, installing, and testing charts across changed directories in a CI pipeline using a dedicated Kind or similar ephemeral cluster.
Detailed Answer
Think of Helm chart testing like quality assurance at a car factory. helm test is the final road test where a real car is driven on a track to verify it starts, accelerates, and brakes correctly. chart-testing (ct) is the entire assembly-line inspection system that checks blueprints for defects, builds the car in a test bay, runs the road test, and then tears down the bay for the next model. Both are necessary: helm test validates a single deployed release, while ct orchestrates the full lifecycle of detecting changed charts, linting them, deploying them to a fresh cluster, testing them, and cleaning up.
helm test works through the test hook mechanism. You create Pod or Job resources in the templates directory annotated with helm.sh/hook: test. When you run helm test followed by a release name, Helm finds all resources with this annotation, creates them in the cluster, and waits for each to complete. If all test pods exit with code zero, the test passes. If any pod exits non-zero, the test fails and Helm reports the failure. Test pods typically run connection checks against services deployed by the chart, execute API health endpoint requests, verify database connectivity, or validate that configuration was applied correctly. The dash-dash-logs flag prints the pod logs to stdout after completion, which is invaluable for debugging failures in CI environments where you cannot interactively inspect pods.
Internally, chart-testing or ct is a standalone binary maintained under the helm/chart-testing GitHub repository. It is designed for monorepo workflows where multiple charts live in the same Git repository. The ct lint command discovers which charts changed by comparing the current branch against a configurable target branch, then runs helm lint and optional YAML schema validation on each changed chart. The ct install command takes this further by spinning up a fresh namespace for each changed chart, running helm install, waiting for all resources to reach ready state, executing helm test against the deployed release, and finally cleaning up by deleting the namespace. The ct.yaml configuration file lets you specify chart directories, target branches, excluded charts, and Helm configuration. This design makes ct the natural fit for CI pipelines using GitHub Actions, GitLab CI, or Jenkins, where you want to validate only the charts that actually changed in a pull request.
In production CI pipelines, the standard pattern combines a Kind cluster with chart-testing in a GitHub Actions workflow. The workflow creates an ephemeral Kubernetes cluster using Kind inside the CI runner, installs the ct binary, and runs ct lint-and-install which performs both linting and full lifecycle testing in a single command. Charts can declare ct-specific test values in a ci directory containing values files named for the chart, allowing tests to use lightweight configurations such as a single replica and no persistent volumes that run quickly in CI. Teams often extend this with conftest or Open Policy Agent to validate rendered manifests against company policies, kubeval or kubeconform to verify Kubernetes schema compliance, and Polaris or Pluto for best-practice and deprecation checks.
A sneaky gotcha is that helm test pods remain in the namespace after test completion unless you specify a deletion policy. In CI pipelines, this is usually handled by ct which deletes the entire namespace, but in long-lived environments, leftover test pods accumulate and confuse monitoring. Always add helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded to test pod annotations. Another common mistake is writing test pods that depend on timing, such as checking a pod log before the application has fully started. Use init containers with retry logic or the wget dash-dash-retry-connrefused flag to handle startup delays gracefully instead of inserting arbitrary sleep commands that make tests flaky and slow.