차트를 위한 RSpec 테스트 작성
다음은 GitLab 차트를 위한 RSpec 테스트를 작성할 때 사용되는 노트와 규칙입니다.
RSpec 테스트 필터링
개발을 지원하기 위해 :focus
태그를 하나 이상의 테스트에 추가하여 실행할 테스트를 필터링할 수 있습니다. :focus
태그가 있는 경우 오직 특정하게 태그가 지정된 테스트만 실행됩니다. 이를 통해 모든 RSpec 테스트를 실행하기를 기다리지 않고 새로운 코드를 신속하게 개발하고 테스트할 수 있습니다. 다음은 :focus
로 태그가 지정된 테스트의 예입니다.
describe 'some feature' do
it 'generates output', :focus => true do
...
end
end
:focus
태그는 describe
, context
또는 it
블록에 추가할 수 있어 테스트 또는 테스트 그룹을 실행할 수 있습니다.
차트에서 YAML 생성하기
차트 테스트의 대부분은 여러 개의 차트 입력을 주어 올바른 YAML 구조를 생성하는 것입니다. 이는 다음과 같이 HelmTemplate 클래스를 사용하여 수행됩니다.
obj = HelmTemplate.new(values)
결과적으로 생성된 obj
는 helm template
명령에 의해 반환된 YAML 문서를 Kubernetes 객체 kind
및 객체 이름(metadata.name
)으로 인덱싱한 것입니다. 이 인덱스 값은 대부분의 메서드에서 YAML 내의 값을 찾는 데 사용됩니다.
예를 들어:
obj.dig('ConfigMap/test-gitaly', 'data', 'config.toml.tpl')
이는 test-gitaly
ConfigMap에 포함된 config.toml.tpl
파일의 내용을 반환합니다.
HelmTemplate
클래스를 사용하면 항상 “test”라는 릴리스 이름을 사용하여 helm template
명령을 실행합니다.차트 입력
HelmTemplate
클래스 생성자의 입력 매개변수는 Helm 명령줄에서 사용되는 values.yaml
을 나타내는 값의 사전입니다. 이 사전은 values.yaml
파일의 YAML 구조를 반영합니다.
describe 'some feature' do
let(:default_values) do
HelmTemplate.defaults
# 또는:
# 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-merge 젬을 사용하고 있습니다.
속성을 추가할 때는 잘 작동했습니다. 단점은 중첩 구조를 덮어쓸 수 있는 방법이 제공되지 않는다는 것입니다.
Helm은 coalesceValues 함수를 통해 구성 속성을 병합/조정하며, 이는 여기서 구현된 deep_merge
와는 몇 가지 뚜렷한 다른 동작을 가지고 있습니다. 우리는 RSpec 내에서 이 기능을 계속 다듬고 있습니다.
일반적인 지침:
-
Hash.merge
의 동작에 유의하고 경계하세요. -
hash-deep-merge
젬이 제공하는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 내에서 사용되는 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
로 설정해야 합니다.
두 번째 관찰: 이는 뚜렷한 차이입니다. YAML에서 해시를 null
로 설정하면 Helm은 전체 키를 제거하지만 부모 유형은 그대로 유지합니다. Ruby는 키를 존재하게 남기되, 값은 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()
. YAML 문서를 생성하기 위해 사용된helm template
명령의 종료 코드를 반환합니다. Kubernetes 클러스터에 차트를 인스턴스화하는 명령입니다.helm template
의 성공적인 완료는 0의 종료 코드를 반환합니다. -
.dig(key, ...)
.HelmTemplate
인스턴스에서 반환된 YAML 문서를 따라 내려가면서 마지막 키에 해당하는 값을 반환합니다. 값이 발견되지 않으면nil
이 반환됩니다. -
.labels(item)
. 지정된 객체에 대한 레이블의 해시를 반환합니다. -
.template_labels(item)
. 지정된 객체에 대한 템플릿 구조에서 사용된 레이블의 해시를 반환합니다. 지정된 객체는 Deployment, 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 템플릿을 실행하며, 이는 시간과 자원을 소모하는 작업입니다. RSpec 테스트 스위트에서 이러한 블록의 빈도가 높기 때문에 가능한 한 it
블록의 수를 줄이는 것을 목표로 합니다.
RSpec 문서는 추가 설명을 제공합니다:
let
을 사용하여 메모이즈된 헬퍼 메서드를 정의하십시오. 값은 동일한 예제에서 여러 호출 간에 캐시되지만 다른 예제 간에는 캐시되지 않습니다.
예를 들어, 다음 테스트 리팩토링을 고려하십시오:
이전: 실행하는 데 약 14초 소요
let(:template) { HelmTemplate.new(deployments_values) }
it '명시하지 않았을 때 전역 수신 제공자를 올바르게 설정합니다' do
expect(template.annotations('Ingress/test-webservice-default')).to include('kubernetes.io/ingress.provider' => 'global-provider')
end
it '명시했을 때 로컬 수신 제공자를 올바르게 설정합니다' 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 '수신 제공자를 올바르게 설정합니다' 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
두 개의 it
블록을 하나로 통합하면 helm template
에 대한 호출 수를 줄이기 때문에 상당한 시간 절약 효과를 얻습니다.