chart-tests

작성자: astronomer

Astronomer APC 저장소의 Helm 차트 테스트를 작성, 편집, 검토 또는 실행할 때 사용합니다. pytest 패턴, render_chart() 사용법, 서브 차트 등을 다룹니다.

npx skills add https://github.com/astronomer/astronomer --skill chart-tests

Chart Test Writing Guide

Critical Rules

  1. Always run tests with uv run — never python3 -m pytest or python bin/...
  2. Sub-chart values MUST be nested under the sub-chart name (see Values Nesting)
  3. No helm unittest plugin — all tests are pytest-based using render_chart()
  4. One render_chart() call per test function — never call it multiple times in the same function to cover different value combinations (see One Condition Per Test)

Test Organization

tests/
├── chart_tests/              # Helm template rendering tests (main focus)
│   ├── test_<component>.py   # One file per component
│   ├── conftest.py           # Shared fixtures
│   └── test_data/            # Feature configs, expected outputs
├── functional/               # End-to-end cluster tests
├── k8s_schema/               # Cached Kubernetes API schemas
└── utils/
    ├── chart.py              # render_chart() and helpers
    ├── fixtures.py           # Common fixtures
    └── __init__.py           # get_containers_by_name(), get_all_features(), etc.

Writing Tests

Basic Pattern

import pytest
from tests import supported_k8s_versions
from tests.utils.chart import render_chart

DEPLOYMENT_FILE = "charts/grafana/templates/grafana-deployment.yaml"


@pytest.mark.parametrize("kube_version", supported_k8s_versions)
def test_some_feature(kube_version):
    """Brief description of what is being tested."""
    docs = render_chart(
        kube_version=kube_version,
        show_only=[DEPLOYMENT_FILE],
        values={"grafana": {"enabled": True}},
    )
    assert len(docs) == 1
    assert docs[0]["kind"] == "Deployment"

Sub-Chart Values Nesting — CRITICAL

Templates in charts/<subchart>/templates/ belong to a sub-chart. Values for those templates must be nested under the sub-chart's top-level key:

# ❌ WRONG — will not override sub-chart values
values = {"houston": {"replicas": 3}}

# ✅ CORRECT — nest under the sub-chart name
values = {"astronomer": {"houston": {"replicas": 3}}}

# ✅ EXAMPLE — disable a feature in the astronomer sub-chart
docs = render_chart(
    values={"astronomer": {"dpLink": {"enabled": False}}},
    show_only=["charts/astronomer/templates/dp-link/dp-link-deployment.yaml"],
)
assert len(docs) == 0

Top-level charts (e.g. nginx, grafana, prometheus) use their chart name directly:

values = {"nginx": {"serviceType": "LoadBalancer"}}
values = {"grafana": {"extraEnvVars": [...]}}

Using show_only

Always use show_only to target the specific template being tested:

docs = render_chart(
    show_only=[
        "charts/nginx/templates/controlplane/nginx-cp-service.yaml",
        "charts/nginx/templates/dataplane/nginx-dp-service.yaml",
    ]
)

Parametrized Tests

Always parametrize over supported_k8s_versions and over relevant values axes:

@pytest.mark.parametrize("kube_version", supported_k8s_versions)
@pytest.mark.parametrize("plane_mode,docs_count", [("control", 1), ("unified", 1), ("data", 0)])
def test_deployment_should_render(kube_version, plane_mode, docs_count):
    docs = render_chart(
        kube_version=kube_version,
        show_only=[DEPLOYMENT_FILE],
        values={"global": {"plane": {"mode": plane_mode}}},
    )
    assert len(docs) == docs_count

One Condition Per Test

Never call render_chart() more than once inside the same test function to check several value combinations (e.g. default, feature-enabled, feature-disabled). Each render_chart() call is its own test condition — hiding several behind one function name makes failures ambiguous (which call failed?) and hides the test matrix from pytest --collect-only / -k filtering. Use parametrization when the calls only differ by a value/expectation, or separate test functions when they differ conceptually:

# ❌ WRONG — three renders buried in one function
def test_secretstore_rule(kube_version):
    docs = render_chart(kube_version=kube_version, show_only=[ROLE_FILE])
    assert absent(docs[0])

    docs = render_chart(kube_version=kube_version, values={"global": {"dataPlaneFailover": {"enabled": True}}}, show_only=[ROLE_FILE])
    assert present(docs[0])

    docs = render_chart(kube_version=kube_version, values={"global": {"dataPlaneFailover": {"enabled": False}}}, show_only=[ROLE_FILE])
    assert absent(docs[0])
# ✅ CORRECT — parametrize when only the value/expectation changes
@pytest.mark.parametrize("kube_version", supported_k8s_versions)
@pytest.mark.parametrize(
    "dataplane_failover_enabled,rule_expected",
    [(None, False), (True, True), (False, False)],
    ids=["default", "enabled", "disabled"],
)
def test_secretstore_rule(kube_version, dataplane_failover_enabled, rule_expected):
    values = {}
    if dataplane_failover_enabled is not None:
        values = {"global": {"dataPlaneFailover": {"enabled": dataplane_failover_enabled}}}
    docs = render_chart(kube_version=kube_version, values=values, show_only=[ROLE_FILE])
    assert (expected_rule in docs[0]["rules"]) == rule_expected

Or, if an existing test function already renders with the exact values you need (e.g. a test for dataPlaneFailover.enabled: True that targets a related template), prefer adding your assertion to that template's doc in the existing show_only list over writing a new render_chart() call.

Testing with All Features Enabled

get_all_features() enables as many compatible features as possible. Not all features can be enabled simultaneously due to incompatibilities.

from tests.utils import get_all_features

def test_with_all_features():
    docs = render_chart(values=get_all_features())
    kinds = [doc["kind"] for doc in docs]
    assert "Deployment" in kinds

Testing Probe Customization

Every container must support customizable livenessProbe and readinessProbe. When adding a new component:

  1. Add its probes to tests/chart_tests/test_data/enable_all_probes.yaml
  2. Run tests with that file to verify probes are rendered correctly

Cross-cutting invariants — ALWAYS guard with a cross-cutting test

Some requirements must hold for every container/pod/object the cluster renders, not just one component — typically because an admission controller (e.g. a Gatekeeper/OPA constraint) rejects the whole install if a single object violates them. Examples: every container must define a startupProbe (allow-with-probes, PINF-691), allowPrivilegeEscalation: false (PINF-585/713), no forbidden Service fields (PINF-692).

For requirements like these, a per-component test is not enough — a new component added later silently reintroduces the violation. Always add a single cross-cutting test that:

  1. Renders with get_all_features() (what a real install presents to the admission controller), and
  2. Iterates every container across every pod-manager kind and asserts the invariant, so any new component that violates it fails the suite automatically.

Use get_chart_containers() or get_containers_by_name() over the filtered doc list. test_probes.py (TestStartupProbes, TestCustomProbes) and test_security_context_override.py are the reference patterns. Write this test first (TDD): it should fail (red) listing every offending container before you implement the fix, then pass (green) once coverage is complete.


ConfigMap Scripts

Scripts embedded in ConfigMaps must follow these conventions:

  1. Static content only — scripts must not use Helm templating to conditionally modify their content based on chart values. The rendered output must be identical regardless of what values are passed.

  2. Environment variable inputs — all runtime configuration must be passed as environment variables defined in the container spec (via env or envFrom), not baked into the script at render time.

  3. Stored as files on disk — scripts must be committed as real files in the repository (e.g. under charts/<subchart>/files/) so they can be linted and reviewed like any other source file.

  4. Included via .Files.Get — scripts must be included in ConfigMap templates using .Files.Get, not inline Helm template blocks:

    # ✅ CORRECT
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: {{ include "chart.fullname" . }}-scripts
    data:
      my-script.sh: {{ .Files.Get "files/my-script.sh" | quote }}
    
    # ❌ WRONG — inline script with template logic
    data:
      my-script.sh: |
        #!/bin/sh
        {{- if .Values.someFlag }}
        do_something
        {{- end }}
    

Test Utilities

render_chart(values, show_only, kube_version, validate_objects)

Renders the chart via helm template and returns parsed YAML documents.

ParameterTypeDescription
valuesdictValues merged with chart defaults
show_onlylist[str]Templates to render (filters output)
kube_versionstrK8s version for schema validation
validate_objectsboolValidate against K8s schemas (default True)
from tests.utils.chart import render_chart

docs = render_chart(
    values={"nginx": {"enabled": True}},
    show_only=["charts/nginx/templates/controlplane/nginx-cp-service.yaml"],
    kube_version="1.31.0",
)

get_containers_by_name(doc, *, include_init_containers=False)

Returns {name: container_dict} for all containers in a pod manager doc (Deployment, StatefulSet, DaemonSet, Job, CronJob). Pass include_init_containers=True to also include init containers.

from tests.utils import get_containers_by_name

c_by_name = get_containers_by_name(doc, include_init_containers=True)
assert c_by_name["grafana"]["securityContext"] == {"readOnlyRootFilesystem": True}
assert c_by_name["bootstrapper"]["securityContext"] == {"readOnlyRootFilesystem": True}

get_all_features()

Returns a values dict with most components enabled.

from tests.utils import get_all_features

Other utilities in tests/utils/__init__.py

  • get_env_vars_dict(container_env) — converts env list to {name: value} dict
  • get_service_ports_by_name(doc) — returns service ports keyed by name
  • get_pod_template(doc) — extracts pod template from any pod manager
  • get_service_account_name_from_doc(doc) — returns the serviceAccountName
  • dot_notation_to_dict(dotted_string, default_value) — builds nested dict from dot notation

Running Tests

Correct examples

# Full suite in parallel (fastest — use for full runs)
uv run pytest tests/chart_tests/ -n auto --quiet

# Full suite, verbose
uv run pytest tests/chart_tests/ --verbose

# Single file
uv run pytest tests/chart_tests/test_grafana.py --verbose

# Tests matching a pattern
uv run pytest tests/chart_tests/ -k "test_service" --verbose

# Single test
uv run pytest tests/chart_tests/test_grafana.py::test_deployment_should_render --verbose

# Verbose output, stop on first failure
uv run pytest tests/chart_tests/ -vv --capture=no --maxfail=1

# Iterate on failures: re-run only last-failed tests
uv run pytest tests/chart_tests/ --maxfail=1 --lf

Tip: -n auto uses all CPU cores. Omit it when running a single file to avoid subprocess overhead.

Incorrect examples

# ❌ WRONG — we do not need to activate the venv manually, and we do not run pytest without `uv run`
.venv/bin/activate && pytest tests/chart_tests/

# ❌ WRONG — do not create virtual environment with python, do not use pip install. Use `uv run` to run all python files in the repo.
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt && .venv/bin/python3 -m pytest tests/chart_tests/

Kubernetes Schema Validation

Tests validate rendered manifests against cached K8s OpenAPI schemas in tests/k8s_schema/v<version>-standalone/. Validation runs by default; disable with validate_objects=False.

def test_custom_resource():
    docs = render_chart(
        show_only=["charts/airflow-operator/templates/crds/airflow.yaml"],
        validate_objects=True,
    )
    for doc in docs:
        assert doc["kind"] == "CustomResourceDefinition"

astronomer의 다른 스킬

airflow
astronomer
Apache Airflow DAG, 실행, 작업 및 시스템 구성을 쿼리, 관리 및 문제 해결합니다. DAG 검사, 실행 관리, 작업 로깅, 구성 쿼리 및 직접 REST API 액세스에 걸쳐 30개 이상의 명령을 지원합니다. 지속적인 구성으로 여러 Airflow 인스턴스를 관리하고 로컬 및 Astro 배포를 자동으로 검색합니다. DAG 실행을 동기식(완료 대기) 또는 비동기식으로 트리거하고, 실패를 진단하고, 재시도를 위해 실행을 지우고, 재시도/맵 인덱스 필터링을 통해 작업 로그에 액세스합니다. 출력...
official
airflow-hitl
astronomer
인간 승인 게이트, 폼 입력, 그리고 지연 가능 연산자를 사용한 Airflow DAG 내 분기 처리. 네 가지 연산자 유형: 승인/거부 결정을 위한 ApprovalOperator, 폼을 통한 다중 옵션 선택을 위한 HITLOperator, 인간 주도 작업 라우팅을 위한 HITLBranchOperator, 폼 데이터 수집을 위한 HITLEntryOperator. 모든 연산자는 지연 가능하며, Airflow UI의 Required Actions 탭 또는 REST API를 통해 인간 응답을 기다리는 동안 작업자 슬롯을 해제합니다. 선택적 기능 지원 포함: 사용자 정의...
official
airflow-state-store
astronomer
Persists task and asset state across retries and DAG runs using Airflow 3.3's AIP-103 key/value stores (`task_state_store`, `asset_state_store`) and the…
official
analyzing-data
astronomer
데이터 웨어하우스에 질의하여 캐시된 패턴과 개념 매핑을 통해 비즈니스 질문에 답변합니다. 반복되는 질문 유형에 대한 패턴 조회 및 캐싱을 지원하며, 결과 기록을 통해 향후 질의를 개선합니다. 개념-테이블 매핑 캐시와 INFORMATION_SCHEMA 또는 코드베이스 grep을 통한 테이블 스키마 탐색을 포함합니다. 분석을 위해 Polars 또는 Pandas DataFrame을 반환하는 run_sql() 및 run_sql_pandas() 커널 함수를 제공합니다. 개념, 패턴 및 테이블 캐시를 관리하기 위한 CLI 명령어와 추가 기능을 포함합니다.
official
annotating-task-lineage
astronomer
Airflow 태스크에 인렛과 아웃렛을 사용하여 데이터 계보를 주석 처리합니다. 입력 및 출력을 데이터베이스, 데이터 웨어하우스, 클라우드 스토리지 전반에 걸쳐 정의하기 위해 OpenLineage Dataset 객체, Airflow Assets 및 Airflow Datasets를 지원합니다. 운영자에 내장된 OpenLineage 추출기가 없는 경우 대체 수단으로 사용되며, 사용자 정의 추출기와 OpenLineage 메서드가 우선 적용되는 4단계 우선순위 시스템을 따릅니다. Snowflake, BigQuery, S3 및 PostgreSQL에 대한 일관된 명명을 보장하는 데이터셋 명명 헬퍼를 포함합니다.
official
authoring-dags
astronomer
Apache Airflow DAG 생성을 위한 안내 워크플로우로, 검증 및 테스트 통합을 포함합니다. 구조화된 6단계 접근 방식: 환경 및 기존 패턴 발견, DAG 구조 계획, 모범 사례에 따른 구현, af CLI 명령어로 검증, 사용자 동의 하에 테스트, 수정 반복. 발견을 위한 CLI 명령어(af config connections, af config providers, af dags list)와 검증을 위한 명령어(af dags errors, af dags get, af dags explore)는 DAG에 대한 즉각적인 피드백을 제공합니다...
official
authoring-go-sdk-tasks
astronomer
Writes Airflow task logic in Go using the Airflow Go SDK. Use when the user wants to implement Airflow tasks in Go, asks about `BundleProvider`/`RegisterDags`,…
official
authoring-java-sdk-tasks
astronomer
Airflow 작업 로직을 Java, Kotlin 또는 Airflow Java SDK를 사용하는 모든 JVM 언어로 작성합니다. 사용자가 Java/JVM에서 Airflow 작업을 구현하려 하거나, 요청할 때 사용합니다…
official