chart-tests

作者: astronomer

在撰寫、編輯、審閱或執行 Astronomer APC 儲存庫的 Helm chart 測試時使用。涵蓋 pytest 模式、render_chart() 用法、子 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-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…
creating-openlineage-extractors
astronomer
為不支援的Airflow運算子及複雜血緣場景設計的自訂OpenLineage提取器。提供兩種方法:直接在你擁有的運算子中加入OpenLineage方法(建議做法),或為無法修改的第三方運算子建立自訂提取器。提取器在三個時間點攔截運算子執行:執行前取得靜態血緣、成功後取得執行階段決定的輸出、以及選擇性地在失敗後取得部分血緣。可透過airflow.cfg或環境變數註冊提取器...
debugging-dags
astronomer
針對失敗的 Airflow DAG 進行系統性根本原因分析與修復,並提供結構化的調查流程。引導完成四個階段的診斷步驟:識別失敗、提取錯誤細節、收集背景資訊,以及提供可行的修復步驟。將失敗分為四種類型(資料、程式碼、基礎設施、相依性),以聚焦調查並建議適當的修正方式。提供可直接使用的 CLI 指令,用於日誌擷取、執行比較、任務清除與 DAG...
delegating-to-otto
astronomer
驅動 Astronomer 的 Otto 代理
deploying-airflow
astronomer
部署 Airflow DAG 和專案。當使用者想要部署程式碼、推送 DAG、設定 CI/CD、部署到生產環境,或詢問部署策略時使用…
deploying-go-sdk-bundles
astronomer
建置、打包並部署已編譯的 Airflow Go SDK 套件,以便 ExecutableCoordinator 能執行它們。當使用者想要編譯 Go 任務套件、要求…時使用。
testing-dags
astronomer
針對Airflow DAG的反覆測試-除錯-修復循環,提供全面的失敗診斷。從af runs trigger-wait <dag_id>開始執行DAG並等待完成,無需預先檢查。失敗時,使用af runs diagnose獲取完整的失敗摘要,並透過af tasks logs檢查特定任務的錯誤細節。支援自訂配置、超時設定與重試機制;能處理成功、失敗及超時情境,並提供清晰的回應解讀。快速驗證功能亦已就緒...
tracing-downstream-lineage
astronomer
追蹤下游資料血緣,在修改資料表或DAG前評估變更影響。透過原始碼搜尋、檢視相依性及BI工具連線,識別目標資料表或DAG的直接消費者。建立完整的相依性樹狀圖,繪製從資料表到儀表板再到機器學習模型的所有下游影響。依關鍵性(關鍵、高、中、低)分類相依性,以優先處理利害關係人溝通與測試。產出包含風險評估、受影響範圍的影響報告。