functional-tests

作成者: astronomer

Astronomer APCリポジトリの機能(エンドツーエンド)テストを作成、編集、レビュー、または実行する際に使用します。シナリオのセットアップ、testinfraパターンなどをカバーします。

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

Functional Test Writing Guide

Overview

Functional tests run against a live Kubernetes cluster (kind) with the Helm chart installed. Unlike chart tests, they verify real runtime behavior: running processes, user identity, network reachability, and configuration values.


Critical Rules

  1. Always run tests with uv run — never python3 -m pytest or python -m pytest
  2. Pass --topology to bin/reset-local-dev (unified/control/data) before running — pytest itself needs no env var, it infers topology from the test's own location on disk
  3. Always use uppercase kubeconfig constants from tests.utils.k8s — KUBECONFIG_UNIFIED, KUBECONFIG_CONTROL, KUBECONFIG_DATA (not lowercase variants)
  4. Run bin/reset-local-dev before the first test run to set up the cluster

Installation Scenarios

Three scenarios exist, each with its own test directory:

ScenarioDirectoryDescription
unifiedtests/functional/unified/Control plane + data plane in one cluster
controltests/functional/control/Control plane components only
datatests/functional/data/Data plane components only

Cross-scenario tests (applicable to all planes) belong in tests/functional/shared/. This directory does not yet exist — create it (with an __init__.py) when adding the first shared test, then add an entry point to each scenario's conftest if needed.


Local Setup Workflow

# 1. Set up the cluster for your chosen topology (downloads tools, generates certs, launches kind, installs chart)
bin/reset-local-dev --topology=unified   # or: control, data

# 2. Run the tests (does NOT tear down the cluster — re-run freely while iterating)
uv run pytest tests/functional/unified

# 3. See helper file paths (kubeconfig, etc.)
make show-test-helper-files

Makefile shortcuts run setup + tests in one step:

make test-functional-unified
make test-functional-control
make test-functional-data

Enable verbose debug output (helm install --debug, kubectl -v=9):

export DEBUG=1

Stored artifacts (outside the repo, consistent across runs):

  • Tools: ~/.local/share/astronomer-software/bin
  • Kubeconfigs: ~/.local/share/astronomer-software/kubeconfig/{unified,control,data}
  • Certs: ~/.local/share/astronomer-software/certs (auto-renewed if expiring within 4 weeks)

Test Organization

tests/functional/
├── conftest.py                        # Shared fixtures (k8s clients, named pod hosts)
├── unified/
│   ├── conftest.py                    # unified-specific fixtures (if any)
│   ├── test_config.py                 # Configuration and behavior assertions
│   ├── test_container_user_is_not_root.py
│   ├── test_network_security.py       # Port-scan test (complex one-off, do not replicate pattern)
│   └── test_container_read_only_root.py
├── control/
│   ├── conftest.py
│   ├── test_control.py
│   ├── test_pod_configs.py
│   └── test_container_user_is_not_root.py
├── data/
│   ├── test_data.py
│   └── test_container_user_is_not_root.py
└── shared/                            # Create when adding first cross-scenario test
    ├── __init__.py
    └── test_<name>.py

Kubeconfig Helpers

Always import from tests.utils.k8s:

from tests.utils.k8s import KUBECONFIG_UNIFIED, KUBECONFIG_CONTROL, KUBECONFIG_DATA

These resolve to ~/.local/share/astronomer-software/kubeconfig/<scenario>.

Known bug: tests/functional/control/test_container_user_is_not_root.py imports kubeconfig_control (lowercase), which does not exist in tests.utils.k8s. Fix this to KUBECONFIG_CONTROL whenever you touch that file.


Shared Fixtures

tests/functional/conftest.py provides these fixtures (all scope="function"):

FixtureTypeDescription
k8s_core_v1_clientCoreV1ApiKubernetes core/v1 API client
k8s_apps_v1_clientAppsV1ApiKubernetes apps/v1 API client
cp_nginxtestinfra.Hostcp-ingress-controller nginx container
dp_nginxtestinfra.Hostdp-ingress-controller nginx container
grafanatestinfra.Hostgrafana container
houston_apitestinfra.Hosthouston container
prometheustestinfra.Hostprometheus-0 container
es_mastertestinfra.Hostelasticsearch-master-0 container
es_datatestinfra.Hostelasticsearch-data-0 container
all_containerslist[testinfra.Host]Every container in the astronomer namespace

Writing Tests

Assert command output in a container

def test_prometheus_user(prometheus):
    user = prometheus.check_output("whoami")
    assert user == "nobody", f"Expected 'nobody', got '{user}'"

Assert a file exists and has expected content

def test_dashboard_config_mounted(grafana):
    f = grafana.file("/etc/grafana/provisioning/dashboards/dashboard.yaml")
    assert f.exists
    assert f.is_file
    content = grafana.check_output("cat /etc/grafana/provisioning/dashboards/dashboard.yaml")
    assert "apiVersion: 1" in content
    assert "providers:" in content

Assert containers do not run as root

import pytest
import testinfra
from tests.utils.k8s import KUBECONFIG_UNIFIED, get_pod_running_containers

container_ignore_list = ["kube-state", "houston", "astro-ui"]


def test_container_user_is_not_root():
    containers = get_pod_running_containers(kubeconfig=KUBECONFIG_UNIFIED, namespace="astronomer")
    for container in containers.values():
        if container["_name"] in container_ignore_list:
            pytest.skip(f"Unsupported container: {container['_name']}")
        host = testinfra.get_host(
            f"kubectl://{container['pod_name']}?container={container['_name']}&namespace={container['namespace']}",
            kubeconfig=KUBECONFIG_UNIFIED,
        )
        user = host.user()
        assert user.name != "root"
        assert user.uid != 0
        assert user.gid != 0

Use the Kubernetes API directly

def test_ensure_feature_disabled(k8s_core_v1_client):
    pods = k8s_core_v1_client.list_namespaced_pod("astronomer")
    should_not_run = ["prometheus-postgres-exporter"]
    for pod in pods.items:
        for feature in should_not_run:
            if feature in pod.metadata.name:
                raise ValueError(f"Expected '{feature}' to be disabled")

Parse JSON config from a container process

import json


def test_houston_config(houston_api):
    data = houston_api.check_output("echo \"config = require('config'); console.log(JSON.stringify(config))\" | node -")
    config = json.loads(data)
    assert "url" not in config["nats"]
    assert len(config["nats"]["servers"]) > 0

Flaky Tests

Use @pytest.mark.flaky for tests that depend on eventually-consistent cluster state (e.g. network reachability, pod readiness):

@pytest.mark.flaky(reruns=20, reruns_delay=10)
def test_houston_can_reach_prometheus(houston_api):
    assert houston_api.check_output("wget --timeout=5 -qO- http://astronomer-prometheus.astronomer.svc.cluster.local:9090/targets")
  • reruns: max retry attempts on failure
  • reruns_delay: seconds between retries
  • Use sparingly — only when the cluster genuinely needs time to converge

Utility Functions

From tests.utils.k8s:

get_pod_running_containers(namespace, kubeconfig=None) -> dict Returns {pod_name_container_name: container_info} for all ready containers. Each value includes pod_name, namespace, and _name (container name).

get_pod_by_label_selector(namespace, label_selector, kubeconfig) -> str Returns the name of the first pod matching the given label selector. Asserts at least one pod is found.


What NOT to Do

  • Do not hardcode kubeconfig paths — always use the constants from tests.utils.k8s
  • Do not run with python -m pytest — always use uv run pytest
  • Do not replicate the class-based structure of test_network_security.py for ordinary tests — that file is a one-off for a specialized port-scan workflow
  • Do not add tests directly to tests/functional/ root — tests belong in a scenario subdirectory or shared/

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
カスタムOpenLineage抽出器。未対応のAirflowオペレーターや複雑な系列シナリオ向け。2つのアプローチ:所有するオペレーターに直接OpenLineageメソッドを追加する方法(推奨)、または変更できないサードパーティ製オペレーター用にカスタム抽出器を作成する方法。抽出器は3つの時点でオペレーターの実行をインターセプトします:静的な系列のための実行前、実行時に決定される出力のための成功後、およびオプションで部分的な系列のための失敗後。抽出器はairflow.cfgまたは環境変数経由で登録...
debugging-dags
astronomer
失敗したAirflow DAGに対する体系的な根本原因分析と修正、構造化された調査ワークフローを提供。4段階の診断プロセス(障害の特定、エラー詳細の抽出、コンテキスト情報の収集、実行可能な修正手順の提示)をガイド。障害を4つのタイプ(データ、コード、インフラストラクチャ、依存関係)に分類し、調査を集中させ適切な修正を提案。ログ取得、実行比較、タスククリア、DAG...のための即時使用可能なCLIコマンドを提供。
delegating-to-otto
astronomer
Drives Astronomer's Otto agent (`astro otto`) as a delegated sub-agent for Airflow, dbt, and data-engineering work. Use when the user explicitly asks to "use…
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の直接的な消費者を特定します。テーブルからダッシュボード、MLモデルに至るまで、すべての下流影響をマッピングする完全な依存関係ツリーを構築します。依存関係を重要度(クリティカル、高、中、低)で分類し、ステークホルダーへの連絡とテストの優先順位付けを行います。リスク評価と影響を受けるものを含む影響レポートを生成します。