Chart用のRSpecテストの記述

以下はGitLabチャート用のRSpecテストを作成する際の注意点と規約です。

RSpec テストのフィルタリング

開発者を支援するために、:focus ひとつあるいは複数の :focusテストにタグを追加して実行するテストを絞り込むことができます。タグを指定:focus すると :focus、タグが指定された_テストのみが_実行されるようになります。これにより、すべての RSpec テストが実行されるのを待つことなく、 新しいコードを素早く開発したりテストしたりすることができます。以下は、:focus でタグ付けされたテストの例です。

describe 'some feature' do
  it 'generates output', :focus => true do
    ...
  end
end

:focus タグは、describecontextit ブロックに追加することで、テストやテストグループを実行することができます。

ChartからYAMLを生成します

Chartのテストの多くは、多くのChart入力に対して正しいYAML構造を生成することです。これは次のように HelmTemplate クラスを使って行われます:

obj = HelmTemplate.new(values)

結果のobjKubernetes オブジェクトkind とオブジェクト名 (metadata.name) によってインデックス付けされたhelm template コマンドによって返された YAML ドキュメントをエンコードします。このインデックス化された値は、YAML内の値を見つけるためにほとんどのメソッドで使用されます。

使用例:

obj.dig('ConfigMap/test-gitaly', 'data', 'config.toml.tpl')

これはtest-gitaly ConfigMap に含まれるconfig.toml.tpl ファイルの内容を返します。

note
HelmTemplate クラスを使用すると、helm template コマンドを実行するときに常に “test” というリリース名が使用されます。

Chartの入力

HelmTemplate クラスのコンストラクタの入力パラメータは、values.yaml Helm コマンドラインで使用さ values.yamlれる値を表す辞書です。values.yaml この辞書 values.yamlはファイルのvalues.yaml YAML 構造を反映 values.yamlします。

describe 'some feature' do
  let(:default_values) do
    HelmTemplate.defaults
    # or:
    # HelmTemplate.with_defaults(%(
    #  yourCustom: values
    #))
  end

  describe 'global.feature.enabled' do
    let(:values) do
      YAML.safe_load(%(
        global:
          feature:
            enabled: true
      )).deep_merge(default_values)
    end

    ...
  end
end

上のスニペットは、複数のテストで共通のデフォルト値を設定し、それを特定のテストのHelmTemplate コンストラクタで使用する最終的な値にマージするという、よくあるパターンを示しています。

プロパティのマージパターンの使用法

このプロジェクトの RSpec では、merge のさまざまな形式を使用することができます。

RubyのネイティブHash.merge は、移動先のキーを_置き換えるだけ_で、オブジェクトを深く探索することはありません。これは、ソースに一致するエントリがある場合、ツリー下のすべてのプロパティが削除されることを意味します。このアドレスに対処するために、私たちはYAMLドキュメントの素朴なディープマージを実行するためにhash-deep-mergegemを使ってきました。プロパティを_追加_するとき、これはうまくいきました。欠点は、ネストした構造を上書きする手段を提供しないことです。

HelmはcoalesceValues関数によって設定プロパティをマージ/合体させますが、ここで実装されているdeep_merge 。私たちはRSpecの中でこの関数をどのように機能させるか改良を続けています。

一般的なガイドライン

  1. Hash.merge の挙動に注意しましょう。
  2. hash-deep-merge gemが提供するHash.deep_merge の動作に注意し、警戒してください。
  3. 特定のキーを上書きする必要がある場合は、_空でない_内容で明示的に上書きしてください。
  4. 特定のキーを削除する必要がある場合は、null に設定します。
  5. 明示的に必要な場合を除き、命令形 (merge!) は使用しないでください。その場合は、理由をコメントしてください。

マージオペレーションに関する考慮事項の内訳

Ruby のHash.mergehash-deep-merge gem のHash.deep_merge の直接比較です。

2.7.2 :002 > require 'yaml'
 => true
2.7.2 :003"> example = YAML.safe_load(%(
2.7.2 :004">   a:
2.7.2 :005">     b: 1
2.7.2 :006">     c: [ 1, 2, 3]
2.7.2 :007 >  ))
 => {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
2.7.2 :008"> source = YAML.safe_load(%(
2.7.2 :009">   a:
2.7.2 :010">     d: "whee"
2.7.2 :011 >  ))
 => {"a"=>{"d"=>"whee"}}
2.7.2 :012 > example.merge(source)
 => {"a"=>{"d"=>"whee"}}
2.7.2 :013 > require 'hash_deep_merge'
2.7.2 :014 > example = {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
 => {"a"=>{"b"=>1, "c"=>[1, 2, 3]}}
2.7.2 :015 > source = {"a"=>{"b"=> 2, "d"=>"whee"}}
 => {"a"=>{"b"=>2, "d"=>"whee"}}
2.7.2 :016 > example.deep_merge(source)
 => {"a"=>{"b"=>2, "c"=>[1, 2, 3], "d"=>"whee"}}

Rubyのvalues.deep_merge(xyz) とHelmのhelm template . -f xyz.yaml の出力を比較して、Helm内でのdeep_mergecoalesceValues の違いを調べてみましょう。HelmとSprigの内部で使用されるgithub.com/imdario/mergo Goモジュールのmerge.WithOverride と同等の動作が望ましいです。

このためのRubyコードは効果的です:

require 'yaml'
require 'hash_deep_merge'

values = YAML.safe_load(File.read('values.yaml'))
xyz = YAML.safe_load(File.read('xyz.yaml'))

puts values.deep_merge(xyz).to_yaml
---
file: values.yaml
gitlab:
  gitaly:
    securityContext:
      user: 1000
      group: 1000
---
file: empty.yaml     # sets `securityContext: {}`
gitlab:
  gitaly:
    securityContext:
      user: 1000
      group: 1000
---
file: null.yaml      # sets `securityContext: null`
gitlab:
  gitaly:
    securityContext:
---
file: null_user.yaml # sets `securityContext.user: null`
gitlab:
  gitaly:
    securityContext:
      user:
      group: 1000

Helmテンプレートに含まれるのは{{ .Values | toYaml }}

---
# Source: example/templates/output.yaml
file: values.yaml
gitlab:
  gitaly:
    securityContext:
      group: 1000
      user: 1000
---
# Source: example/templates/output.yaml
file: empty.yaml     # sets `securityContext: {}`
gitlab:
  gitaly:
    securityContext:
      group: 1000
      user: 1000
---
# Source: example/templates/output.yaml
file: null.yaml      # sets `securityContext: null`
gitlab:
  gitaly: {}
---
# Source: example/templates/output.yaml
file: null_user.yaml # sets `securityContext.user: null`
gitlab:
  gitaly:
    securityContext:
      group: 1000

最初の観察空の “ハッシュ({})をセットした場合、RubyパターンもHelmパターンも結果は変わりません。これはベース値と “新しい “値が同じ型だからです。ハッシュを_削除_するには、null に設定する必要があります。

2つ目の観察:これは明らかな違いです。YAML でハッシュをnull に設定すると、わずかに異なる結果が得られます。Helmはキー全体を削除しますが、親の型はそのまま残します。Rubyはキーは残しますが、nil 値は nil残します。nil 個々のキーを変更した場合も同様です。Helmはこのキーを削除しますが、Rubyは nilキーの状態を保持します。

最後に!スカラーとマップを混同しないでください。次の YAML を Ruby もしくは Helm でマージすると、配列は[] になります。deep_mergecoalesceValues も配列には入りません。スカラーデータは_上書きさ_れます。

---
complex:
  array: [1,2,3]
  hash:
    item: 1
---
complex:
  array: []
  hash:
    item:
---
# Ruby: puts values.deep_merge(xyz).to_yaml
complex:
  array: []
  hash:
    item:
---
# Source: example/templates/output.yaml
complex:
  array: []
  hash: {}

結果のテスト

HelmTemplate オブジェクトには、RSpec のテストを書く際に役立つメソッドがいくつもあります。利用可能なメソッドを以下にまとめます。

  • .exit_code()

これは、helm template KubernetesクラスタでChartをインスタンス化するYAMLドキュメントを作成するために使用されたコマンドの helm template終了コードを返します。helm template 正常に終了 helm templateすると、終了コード 0 が返されます。

  • .dig(key, ...)

HelmTemplate インスタンスによって返された YAML ドキュメントをウォークダウンし、最後のキーに存在する値を返します。値が見つからない場合は、nil

  • .labels(item)

指定したオブジェクトのラベルのハッシュを返します。

  • .template_labels(item)

指定されたオブジェクトのテンプレート構造で使用されているラベルのハッシュを返します。指定されたオブジェクトはデプロイ、StatefulSet、またはCronJobオブジェクトである必要があります。

  • .annotations(item)

指定されたオブジェクトの注釈のハッシュを返します。

  • .template_annotations(item)

指定されたオブジェクトのテンプレート構造で使用されているアノテーションのハッシュを返します。指定されたオブジェクトはDeployment、StatefulSet、またはCronJobオブジェクトでなければなりません。

  • .volumes(item)

指定されたデプロイオブジェクトのすべてのボリュームの配列を返します。返される配列は、デプロイ オブジェクトからvolumes キーを直接コピーしたものです。

  • .find_volume(item, volume_name)

指定されたデプロイオブジェクトの指定されたボリュームの辞書を返します。

  • .projected_volume_sources(item, mount_name)

指定された投影ボリュームのソースの配列を返します。返される配列の構造は以下のとおりです:

- secret:
    name: test-rails-secret
    items:
     - key: secrets.yml
       path: rails-secrets/secrets.yml
  • .stderr()

helm template コマンドの実行による STDERR 出力を返します。

  • .values()

helm template コマンドの実行で使用されたすべての値の辞書を返します。

Kubernetesクラスタを必要とするテスト

RSpecテストの大部分はhelm template 、生成されたYAMLを解析してテスト対象の機能が正しい構造であるかどうかを確認します。時折、RSpecテストはGitLab HelmチャートがデプロイされたKubernetesクラスタへのアクセスを必要とします。Kubernetes クラスターにデプロイされたチャートとやりとりするテストは、features ディレクトリに配置する必要があります。

RSpecテストが実行されていてKubernetesクラスタが利用できない場合、features ディレクトリにあるテストはスキップされます。RSpecの実行開始時にkubectl get nodes の結果がチェックされ、正常に返された場合はfeatures ディレクトリのテストが含まれます。

テスト速度の最適化

it 各ブロックはHelmテンプレートを実行しますが、これは時間とリソースを消費するオペレーション itです。it RSpecのテストスイートではこれらのブロックの頻度が it高いため、可能な限りブロック数を減らすit ことを目指して itいます。

RSpec のドキュメントに詳しい説明があります:

メモ化されたヘルパーメソッドを定義するにはlet を使用します。この値は、同じサンプル内で複数回呼び出された場合にキャッシュされますが、複数のサンプルにまたがってキャッシュされることはありません。

たとえば、このテストのリファクタリングを考えてみましょう:

実行前:~14秒

let(:template) { HelmTemplate.new(deployments_values) }

it 'properly sets the global ingress provider when not specified' do
  expect(template.annotations('Ingress/test-webservice-default')).to include('kubernetes.io/ingress.provider' => 'global-provider')
end

it 'properly sets the local ingress provider when specified' do
  expect(template.annotations('Ingress/test-webservice-second')).to include('kubernetes.io/ingress.provider' => 'second-provider')
end

実行後~走行時間:~5秒

let(:template) { HelmTemplate.new(deployments_values) }

it 'properly sets the ingress provider' do
  expect(template.annotations('Ingress/test-webservice-default')).to include('kubernetes.io/ingress.provider' => 'global-provider')
  expect(template.annotations('Ingress/test-webservice-second')).to include('kubernetes.io/ingress.provider' => 'second-provider')
end

2つのit ブロックを1つに統合すると、helm template への呼び出し回数が減るため、大幅な時間短縮につながります。