Chart用のRSpecテストの記述
以下はGitLabチャート用のRSpecテストを作成する際の注意点と規約です。
RSpec テストのフィルタリング
開発者を支援するために、:focus
ひとつあるいは複数の :focus
テストにタグを追加して実行するテストを絞り込むことができます。タグを指定:focus
すると :focus
、タグが指定された_テストのみが_実行されるようになります。これにより、すべての RSpec テストが実行されるのを待つことなく、 新しいコードを素早く開発したりテストしたりすることができます。以下は、:focus
でタグ付けされたテストの例です。
describe 'some feature' do
it 'generates output', :focus => true do
...
end
end
:focus
タグは、describe
、context
、it
ブロックに追加することで、テストやテストグループを実行することができます。
ChartからYAMLを生成します
Chartのテストの多くは、多くのChart入力に対して正しいYAML構造を生成することです。これは次のように HelmTemplate クラスを使って行われます:
obj = HelmTemplate.new(values)
結果のobj
はKubernetes オブジェクトkind
とオブジェクト名 (metadata.name
) によってインデックス付けされたhelm template
コマンドによって返された YAML ドキュメントをエンコードします。このインデックス化された値は、YAML内の値を見つけるためにほとんどのメソッドで使用されます。
使用例:
obj.dig('ConfigMap/test-gitaly', 'data', 'config.toml.tpl')
これはtest-gitaly
ConfigMap に含まれるconfig.toml.tpl
ファイルの内容を返します。
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の中でこの関数をどのように機能させるか改良を続けています。
一般的なガイドライン
-
Hash.merge
の挙動に注意しましょう。 -
hash-deep-merge
gemが提供するHash.deep_merge
の動作に注意し、警戒してください。 - 特定のキーを上書きする必要がある場合は、_空でない_内容で明示的に上書きしてください。
- 特定のキーを削除する必要がある場合は、
null
に設定します。 - 明示的に必要な場合を除き、命令形 (
merge!
) は使用しないでください。その場合は、理由をコメントしてください。
マージオペレーションに関する考慮事項の内訳
Ruby のHash.merge
とhash-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_merge
とcoalesceValues
の違いを調べてみましょう。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_merge
もcoalesceValues
も配列には入りません。スカラーデータは_上書きさ_れます。
---
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
への呼び出し回数が減るため、大幅な時間短縮につながります。