pytest-airflow-in-a-box
pytest-airflow-in-a-box is a pytest plugin for testing Apache Airflow DAGs without a live
Airflow deployment. It targets Airflow 3 and provides the package and plugin foundation for a
small, typed testing surface.
The package auto-registers with pytest, creates an isolated metadata database, and provides typed fixtures for persisted Dags, DagRuns, task instances, sessions, and Dag bags.
Requirements
- CPython 3.10 through 3.14
- Apache Airflow 3.1 or newer, below 4
- Linux or macOS for Airflow-backed tests
Apache Airflow does not support native Windows installations. Windows development should use WSL2 or the included devcontainer; platform-independent package checks alone do not imply full Windows Airflow support.
The released compatibility matrix is exercised against Airflow 3.1.0, 3.1.8, 3.2.0, 3.2.2, and 3.3.0 across CPython 3.10 through 3.14 using Airflow's published constraints files.
Installation
uv add --dev pytest-airflow-in-a-box
The pytest11 entry point loads the plugin automatically. Consumer projects do not need to add a
pytest_plugins declaration.
Development
uv sync
uv run prek install
make all
Run the GitHub Actions workflow locally on Linux with act:
act pull_request
act cannot reproduce native macOS or Windows behavior.
Task execution
from airflow.sdk import task
from airflow.utils.state import TaskInstanceState
from pytest_airflow_in_a_box.taskinstance import ordered_task_instances
def test_task(dag_maker):
with dag_maker() as dag:
@task
def answer():
return 42
answer()
dag_run = dag_maker.create_dagrun()
ti = dag_maker.run_ti("answer", dag_run)
assert ti.state == TaskInstanceState.SUCCESS
assert ti.xcom_pull(task_ids="answer", session=dag_maker.session) == 42
assert ordered_task_instances(dag_run, dag, session=dag_maker.session) == [ti]
Public task helpers live in pytest_airflow_in_a_box.taskinstance: run_task_instance,
ordered_task_instances, and TaskResolutionError. The DagMaker protocol additionally exposes
create_dagrun, create_ti, and run_ti.
DB-free task execution
run_task executes one operator through the Task SDK in process, with no metadata database. XCom,
Variable, and Connection traffic is answered from seeded dictionaries; unseeded lookups fail
exactly like a live deployment. Task callbacks and listeners stay silent unless the call passes
run_callbacks=True.
def test_operator(run_task):
result = run_task(
my_operator,
variables={"answer": "42"},
connections={"db": {"conn_type": "postgres", "host": "example.com"}},
)
assert result.state == TaskInstanceState.SUCCESS
assert result.xcoms["return_value"] == "expected"
Structlog capture
Airflow 3 logs through structlog, where pytest's builtin caplog cannot see records. The
cap_structlog fixture records every event emitted during the test:
def test_logging(cap_structlog, dag_maker):
...
assert "task_event" in cap_structlog
assert {"answer": 42, "log_level": "warning"} in cap_structlog
Dag-file collection
Point the collector at a directory of real Dag files and every *.py file below it is collected
as a dag-import test item that fails on import errors or a Dag-free file. Off unless configured:
pytest --collect-dag-folder=dags/
or persistently via the airflow_collect_dags_folder ini option. Collected items are auto-marked
db_test; files also matching test_*.py naming are deduplicated against pytest's default Python
collector.
A Dag file may pin param cases through a module-level literal, read without importing the file:
PYTEST_DAG_CASES = {
"dev": {"environment": "dev"},
"prod": {"environment": "prod"},
}
Each case collects as a sibling dag-params[...] item that validates the pinned values against
every Dag the file declares -- undeclared keys and schema violations fail the case.
Database cleanup
clear_db is a registry-driven whole-database reset for serial setup and teardown contexts:
from pytest_airflow_in_a_box.db import TableGroup, clear_db
clear_db() # every group
clear_db(tables={TableGroup.VARIABLES}) # one group
Requesting a group also clears the groups whose rows reference it (RUNS clears task instances
and XCom rows), and clearing CONNECTIONS recreates Airflow's default connections.
Live REST API
api_client lazily starts one isolated airflow api-server per test process on a loopback
ephemeral port and returns a typed client authenticated through SimpleAuthManager:
import pytest
@pytest.mark.api_test
def test_api(api_client, dag_maker):
with dag_maker(dag_id="visible"):
...
response = api_client.get("/api/v2/dags/visible")
assert response.status == 200
assert response.body["dag_id"] == "visible"
Markers
db_test: requires the isolated metadata databaseapi_test: requires the isolated REST API servercompat: end-user tests exercised across the version matrixneed_serialized_dag([enabled]): request serialized Dag behavior fromdag_makerenvironment(name): run only when the named environment's sentinel path exists, configured via theairflow_environmentsini line list (lab = /opt/lab/sentinel)
Defaults
The plugin needs zero ini configuration. It applies --tb=short, -ra, --durations=20, and
failed-only tmp_path retention, but only where the user has not chosen a value -- explicit flags
and ini settings always win. Warning filters silence traced third-party deprecation noise
(flask_appbuilder, flask_sqlalchemy, starlette) while keeping Airflow's own deprecation
warnings visible, and promote pytest's collection and unraisable warnings to errors. User-supplied
filterwarnings lines take precedence.
License
Apache License 2.0. See LICENSE, NOTICE, and PROVENANCE.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytest_airflow_in_a_box-0.1.2.tar.gz.
File metadata
- Download URL: pytest_airflow_in_a_box-0.1.2.tar.gz
- Upload date:
- Size: 64.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43771adc6497b1a6dbb515d09f39053cb2321fbcce411bbf7b1341e294e637ea
|
|
| MD5 |
eebba4de326388657bb31b10e244454d
|
|
| BLAKE2b-256 |
96515274ea1a9d7615addc9dfb99c4d18a2e54e17a55e32af24915e63dc88411
|
Provenance
The following attestation bundles were made for pytest_airflow_in_a_box-0.1.2.tar.gz:
Publisher:
release.yml on nredd/pytest-airflow-in-a-box
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_airflow_in_a_box-0.1.2.tar.gz -
Subject digest:
43771adc6497b1a6dbb515d09f39053cb2321fbcce411bbf7b1341e294e637ea - Sigstore transparency entry: 2375825264
- Sigstore integration time:
-
Permalink:
nredd/pytest-airflow-in-a-box@abe1ccd0dd6bca0df6b86c7a6b9ba3a551397378 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/nredd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@abe1ccd0dd6bca0df6b86c7a6b9ba3a551397378 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pytest_airflow_in_a_box-0.1.2-py3-none-any.whl.
File metadata
- Download URL: pytest_airflow_in_a_box-0.1.2-py3-none-any.whl
- Upload date:
- Size: 82.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9eccfa8d0585b76371b4812d7f2359d9377e8f894de79181cf5cdcd3097aa8d8
|
|
| MD5 |
d011747f03c7fc16ce27862098314beb
|
|
| BLAKE2b-256 |
7e7869c910f8bf61f39c1373a956cd20920b5ea01dfb9642c7b3ea30b54c2b13
|
Provenance
The following attestation bundles were made for pytest_airflow_in_a_box-0.1.2-py3-none-any.whl:
Publisher:
release.yml on nredd/pytest-airflow-in-a-box
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytest_airflow_in_a_box-0.1.2-py3-none-any.whl -
Subject digest:
9eccfa8d0585b76371b4812d7f2359d9377e8f894de79181cf5cdcd3097aa8d8 - Sigstore transparency entry: 2375825296
- Sigstore integration time:
-
Permalink:
nredd/pytest-airflow-in-a-box@abe1ccd0dd6bca0df6b86c7a6b9ba3a551397378 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/nredd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@abe1ccd0dd6bca0df6b86c7a6b9ba3a551397378 -
Trigger Event:
release
-
Statement type: