functional-tests

À utiliser lors de la rédaction, l'édition, la révision ou l'exécution de tests fonctionnels (de bout en bout) pour le dépôt Astronomer APC. Couvre la configuration de scénarios, les motifs 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/

Plus de skills de 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
Extracteurs OpenLineage personnalisés pour les opérateurs Airflow non pris en charge et les scénarios de lignage complexes. Deux approches : ajouter des méthodes OpenLineage directement aux opérateurs que vous possédez (recommandé), ou créer des extracteurs personnalisés pour les opérateurs tiers que vous ne pouvez pas modifier. Les extracteurs interceptent l'exécution des opérateurs à trois moments : avant l'exécution pour le lignage statique, après le succès pour les sorties déterminées à l'exécution, et optionnellement après l'échec pour un lignage partiel. Enregistrez les extracteurs via airflow.cfg ou l'environnement...
debugging-dags
astronomer
Analyse systématique des causes profondes et correction des DAG Airflow défaillants, avec des flux d'investigation structurés. Guide à travers un processus de diagnostic en quatre étapes : identifier l'échec, extraire les détails de l'erreur, rassembler les informations contextuelles et fournir des étapes de correction exploitables. Classe les échecs en quatre types (données, code, infrastructure, dépendance) pour cibler l'investigation et suggérer les correctifs appropriés. Fournit des commandes CLI prêtes à l'emploi pour la récupération des logs, la comparaison des exécutions, l'effacement des tâches et des DAG...
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
Déployer des DAGs et projets Airflow. Utiliser lorsque l'utilisateur souhaite déployer du code, pousser des DAGs, configurer CI/CD, déployer en production, ou demande des stratégies de déploiement…
deploying-go-sdk-bundles
astronomer
Construit, empaquette et déploie des bundles compilés du SDK Go Airflow afin que l'ExecutableCoordinator puisse les exécuter. À utiliser lorsque l'utilisateur souhaite compiler un bundle de tâches Go, demande…
testing-dags
astronomer
Cycles itératifs de test-débogage-correction pour les DAGs Airflow avec diagnostic complet des échecs. Commencez par af runs trigger-wait <dag_id> pour exécuter un DAG et attendre son achèvement ; aucune vérification préalable nécessaire. En cas d'échec, utilisez af runs diagnose pour un résumé complet des échecs et af tasks logs pour inspecter les détails des erreurs de tâches spécifiques. Prend en charge la configuration personnalisée, les délais d'attente et les tentatives de réessai ; gère les scénarios de succès, d'échec et de dépassement de délai avec une interprétation claire des réponses. Validation rapide disponible...
tracing-downstream-lineage
astronomer
Tracer la lignée des données en aval pour évaluer l'impact des modifications avant de modifier des tables ou des DAG. Identifie les consommateurs directs d'une table ou d'un DAG cible via la recherche dans le code source, les dépendances de vues et les connexions aux outils BI. Construit un arbre de dépendances complet cartographiant tous les impacts en aval, des tables aux tableaux de bord en passant par les modèles ML. Catégorise les dépendances par criticité (critique, élevée, moyenne, faible) pour prioriser la communication avec les parties prenantes et les tests. Génère un rapport d'impact avec évaluation des risques, éléments affectés...