Helm Chart Unit Testing

I really like to add some unit tests when creating Helm Charts, especially when the number of conditional statements increases the complexity grows. So IMHO, it’s crucial to have a safety net to protect your future self from making any mistakes.

You already have helm lint and tools like helm/chart-testing, which are valuable tools when developing Helm Charts. However, they don’t cover all the aspects of a chart, and testing all the conditionals is quite tricky and convoluted.

To my surprise, I couldn’t find a lot of existing projects who try to solve unit testing for Helm Charts. And to be honest, none of the existing solutions were viable alternatives that satisfied me, so I’ve set up a test suite using MiniTest Ruby Test framework instead.

Without the actual tests, the boilerplate code is less than a hundred lines of code, which makes our lives easier. In three easy steps, I’ll walk you through how you will be able to use the same setup yourself.

TL;DR: Check out the test_helper.rb and web_spec.rb in the niels-s/helm-chart-unit-test-example repository.

The Chart Class

The below snippet shows the meat of the Chart class created to abstract away the necessities to gather input values for the Helm chart and to execute the helm template command. It will capture and parse the command’s output to quickly lookup Kubernetes resources to compare them in our unit tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Chart
  ...

  def execute_helm_template!
    file = Tempfile.new(name)
    file.write(JSON.parse(values.to_json).to_yaml)
    file.close

    begin
      command = "helm template '#{name}' '#{path}' --namespace='default' --values='#{file.path}'"
      stdout, stderr, status = Open3.capture3(command)

      raise HelmCompileError, stderr if status != 0

      stdout
    ensure
      file.unlink
    end
  end

  def parsed_resources
    @parsed_resources ||= begin
      output = execute_helm_template!
      puts output if ENV.fetch('DEBUG', 'false') == 'true'
      YAML.load_stream(output)
    end
  end

  def resources(matcher = nil)
    return parsed_resources unless matcher

    parsed_resources.select do |r|
      r >= Hash[matcher.map { |k, v| [k.to_s, v] }]
    end
  end

  ...
end

Extending MiniTest::Spec

Besides the Chart class, I’ve also extended the MiniTest::Spec class to make it easier to interact directly with the “subject” chart. A convenience helper is the jq method, which uses jq to look up specific values using a JSON path query.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
module Minitest
  class Spec
    before { chart.reset! }

    def chart
      subject
    end

    def resource(name)
      chart.resources(kind: name).first
    end

    def jq(matcher, object)
      value(object.jq(matcher)[0])
    end
  end
end

Writing a first test

1
2
3
4
5
6
7
8
9
describe Chart.new('charts/web') do
  describe 'pod replicas' do
    it 'configures 3 replicas' do
      chart.value(replicaCount: 3)

      jq('.spec.replicas', resource('Deployment')).must_equal 3
    end
  end
end

We start the test by first defining which Helm Chart we are testing. In this case, we are testing a simple web chart, see line 1.

In our tests, we can refer to the chart object, which gives us the option to define custom values. As you can see in the replica’s test where we define a custom replicaCount of three on line 4.

And on line 6, we use the jq helper to retrieve the replicas field from the Deployment resource to validate the custom replica count in configured. Calling the resource method will implicitly trigger the helm template action to be performed with the configured values.

Complete example

For a complete example, I’ve set up an example repository, including the chart/web I refer to above and some more unit tests. Have a look at niels-s/helm-chart-unit-test-example.

And let’s give credit where credit’s due to my former colleague Jean Mertz who started with this idea in the first place.