Skip to main content

pytest-airflow-in-a-box

CI coverage PyPI Python versions License Docs Airflow

pytest-airflow-in-a-box is a pytest plugin for testing Apache Airflow DAGs without a live Airflow deployment. It provides the package + plugin foundation for a small, typed testing surface. It primarily targets Airflow 3 but maintains a certified Airflow 2.7-2.11 compatibility tier.

It is for testing the Airflow code you wrote -- your Dags, plus the custom operators, hooks, sensors, decorators, and connection types they lean on -- rather than Airflow's own machinery. See What to test for where that line falls.

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.

Contents

Quickstart

uv add --dev "pytest-airflow-in-a-box[airflow3]"
pip install "pytest-airflow-in-a-box[airflow3]"

Point the plugin at your repo's dags/ folder and run a real Dag end to end:

def test_my_dag(full_dag_bag, run_dag):
    dag = full_dag_bag.dags["my_dag_id"]

    result = run_dag(dag)

    assert result.success
    assert result.order == ["extract", "load"]
pytest --dag-folder=dags

Testing an adhoc Dag

For a Dag authored directly in the test, rather than loaded from your dags/ folder, dag_maker builds and persists one for you:

from airflow.sdk import task


def test_dag(dag_maker):
    with dag_maker():

        @task
        def produce():
            return 21

        @task
        def consume(value):
            return value * 2

        consume(produce())

    result = dag_maker.run()

    assert result.success
    assert result.xcoms == {"produce": 21, "consume": 42}
    assert result.order == ["produce", "consume"]
pytest

run_dag() and dag_maker.run() both return the same inert DagRunResult snapshot: states, xcoms, errors, order, and per-task access via result["task_id"]. Single tasks run with dag_maker.run_ti("produce"), and pytest_airflow_in_a_box.matchers supports one-expression bulk assertions like assert result == {"produce": succeeded(21), "consume": succeeded(42)}.

The pytest11 entry point registers the plugin automatically -- no pytest_plugins declaration needed. See the documentation site for the full dag_maker/run_dag/run/run_ti surface, sessions, DB-free task execution, deferrable operators, the REST API fixture, and bundled smoke checks.

Why not...

  • dag.test() -- Airflow's own built-in helper runs one Dag end to end, but it is not a pytest plugin: no fixtures, no isolated metadata database, no xdist parallelism, no REST API testing
  • upstream tests_common -- the harness Airflow's own core test suite runs on; it targets testing Airflow itself, not published as a package for testing DAG-author code
  • Flowminder pytest-airflow -- an inverse concept (runs pytest suites under Airflow, rather than testing DAGs under pytest) and unmaintained
  • airflow-pytest-plugin -- generates JUnit-XML dashboards from DAG runs; not aimed at isolated, fixture-driven unit testing
  • a dagbag import test plus calling task.function directly -- covers "the Dag parses" and "the callable works" and nothing in between: task relations, cross-Dag asset triggering, DagRun-to-DagRun relations, and retry behavior. See the cookbook for what it misses

Requirements

  • pytest 8 or newer
  • 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 in CI against every combination below, using Airflow's published constraints files:

Tier Airflow versions Python OS Metadata DB
3.x (primary) 3.1.0, 3.1.1, 3.1.2, 3.1.3, 3.1.5, 3.1.6, 3.1.7, 3.1.8, 3.2.0, 3.2.1, 3.2.2, 3.3.0, 3.3.1 3.10 - 3.14 Linux (glibc, musl, arm64), macOS SQLite (WAL), Postgres (testcontainers)
2.x (certified, #25) 2.7.3, 2.8.4, 2.9.3, 2.10.5, 2.11.2 3.10 - 3.12 on 2.9+, 3.10 - 3.11 on 2.7/2.8 (Airflow 2.x never supported 3.13+) Linux SQLite (WAL)

On the 2.x family, run_task, render_task, cap_structlog, and the REST API fixtures fail with actionable errors naming the 2.x alternative; the requires_airflow2/requires_airflow3 markers auto-skip on the other family so one suite runs green on both sides of a migration. The 2.x tier is exercised through the end-user consumer contract (tests/enduser, marked compat) rather than the full internal suite.

Installation

uv add --dev "pytest-airflow-in-a-box[airflow3]"
pip install "pytest-airflow-in-a-box[airflow3]"

The plugin does not depend on Airflow directly: the Airflow 2.x monolith and the 3.x core both install the airflow package, so a hard plugin pin would corrupt whichever family you did not choose. The airflow3 extra pins apache-airflow>=3.1,<4 (the meta-package resolves a coherent core + task-sdk pair). Projects that already pin Airflow themselves -- for example through Airflow's published constraints files -- can install the plugin bare:

pip install pytest-airflow-in-a-box

The airflow2 extra (apache-airflow>=2.7,<3, carrying an explicit python_version < '3.13' marker because Airflow 2.x never supported 3.13 -- on newer interpreters the extra resolves to nothing and the plugin's runtime check names the fix) installs the certified Airflow 2.x compatibility tier (#25): dag_maker (including whole-DagRun execution through dag_maker.run()), run_ti, full_dag_bag, run_dag, clear_db, seeding, and the bundled smoke checks run against 2.7.3, 2.8.4, 2.9.3, 2.10.5, and 2.11.2. The marker is the family-wide cap; 2.7.3 and 2.8.4 cap lower still, at 3.11, and the plugin's runtime check names the offending release. Requesting both Airflow extras together fails at resolution for pip and uv alike, since the apache-airflow version ranges are disjoint.

The pytest11 entry point loads the plugin automatically. Consumer projects do not need to add a pytest_plugins declaration.

The bundled pytest plugins are intentional runtime dependencies. pytest-xdist is part of the supported execution model: controller bootstrap state and worker-scoped artifacts are coordinated for parallel runs. pytest-timeout backs up Airflow's per-file Dag parse watchdog with a corpus-scaled deadline on every bundled smoke item, so whichever worker produces the shared corpus cannot wedge the test session outside the per-file parser boundary.

The plugin is inert on runs without Airflow-facing tests: session startup only prepares a disposable run directory and AIRFLOW__* environment variables. Airflow itself is imported and the metadata database migrated lazily, on the first test that carries a db_test/api_test marker or uses a database-backed plugin fixture. A pytest -k unrelated run in a shared venv never pays the Airflow import or migration cost. Tests that touch the metadata database directly (their own create_session calls, for example) without a plugin fixture must carry db_test to trigger initialization.

To disable the plugin entirely for a run:

pytest -p no:pytest_airflow_in_a_box

GitHub Action

A composite GitHub Action wraps the constraints-pinned uv + Airflow setup this repo's own compat matrix uses, for a Dag repo's own CI. It provisions the environment and stops -- you always write the invocation, so the example below shows several distinct features of the plugin rather than baking one blessed command into the action.

- uses: actions/checkout@v5
- uses: nredd/pytest-airflow-in-a-box/action@v0
  id: airflow-env
  with:
    airflow-version: "3.3.0"
    python-version: "3.12"

# Plain unit tests
- run: ${{ steps.airflow-env.outputs.python-path }} -m pytest

# Bundled smoke checks (Dag import + parse-time diagnostics)
- run: ${{ steps.airflow-env.outputs.python-path }} -m pytest --airflow-smoke

# Migration outcome diff: record a baseline, then compare on a later run
- run: ${{ steps.airflow-env.outputs.python-path }} -m pytest --airflow-record=baseline.json
- run: ${{ steps.airflow-env.outputs.python-path }} -m pytest --airflow-baseline=baseline.json

# The `airflow-migration-diff` console script, via the venv-path output
- run: ${{ steps.airflow-env.outputs.venv-path }}/bin/airflow-migration-diff --project-dir .

Drop it straight into a strategy.matrix loop -- it's a single step with scalar inputs:

jobs:
  test:
    strategy:
      matrix:
        airflow-version: ["3.2.2", "3.3.0", "3.3.1"]
        python-version: ["3.11", "3.12", "3.13"]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: nredd/pytest-airflow-in-a-box/action@v0
        id: airflow-env
        with:
          airflow-version: ${{ matrix.airflow-version }}
          python-version: ${{ matrix.python-version }}
      - run: ${{ steps.airflow-env.outputs.python-path }} -m pytest
Input Required Default Description
airflow-version yes -- Exact Apache Airflow version to install.
python-version yes -- Python version to provision.
extra no airflow3 Plugin extra to install: airflow3 or airflow2.
plugin-version no latest Exact pytest-airflow-in-a-box version to install.
uv-version no 0.12.2 uv version to install.
working-directory no . Directory to run in.
requirements-file no (none) Extra requirements file to install into the same environment.
report-dir no (none) Directory for pytest.log/pytest.xml, appended to PYTEST_ADDOPTS.

Outputs: python-path (the provisioned venv's python), venv-path (the venv directory, for console scripts), and report-dir (the absolute report directory, for an upload step). The examples above pin @v0: release.yml moves a v<major> tag (v0 while pre-1.0, v1 once 1.0.0 ships) to the latest published release on that major line, so @v0 tracks the newest 0.x. Pin a full release tag (e.g. @v0.7.0) for an exact, non-moving reference.

Migration diff orchestrator

airflow-migration-diff is a console script that uv-provisions a disposable Airflow 2.x environment and a disposable Airflow 3.x environment, records outcomes on each, and prints the categorized migration diff -- one command that tells a migrating team exactly what breaks:

airflow-migration-diff --project-dir . -- -k "not slow"

Exit code 0 means no regressions, 1 means at least one was found, and 2 means the orchestrator itself failed (missing uv, a provisioning failure, and the like). See the documentation site for the full option reference.

Documentation

What to test (and what not to), task execution, deferrable operators, DB-free execution, Variable/Connection seeding, structlog capture, Dag collection, configuration overrides, smoke tests, report artifacts, database backends and cleanup, the live REST API, the migration outcome diff, markers, and diagnostics are all covered on the documentation site.

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. See CONTRIBUTING.md for the full contribution workflow and the issue tracker for open work.

License

Apache License 2.0. See LICENSE, NOTICE, and PROVENANCE.md.

Manifesto

In 2024, I learned that my team was abandoning Jenkins for our nightly regressions. A righteous tear rolled down my cheek when I heard the replacement was Airflow: a Python-native workflow platform. As a lover of all things slick and hyper-engineered, I was overjoyed to rewrite all those DISGUSTING unversioned shell scripts into a beautiful library of documented, statically-analyzed, and unit-tested code. Fast forward a few months--I have some crazy 500+ task DAG templates underway (for convoluted semiconductor design methodologies) that were IMPOSSIBLE to fully verify outside of a live Airflow instance. I yearned for a far-off land where I could develop alone in my teched-out Python cave, talk to absolutely no one, and ship complete Methodologies without a whisper in the night. This plugin is the closest thing we have 🫡

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_airflow_in_a_box-0.8.0.tar.gz (203.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_airflow_in_a_box-0.8.0-py3-none-any.whl (237.3 kB view details)

Uploaded Python 3

File details

Details for the file pytest_airflow_in_a_box-0.8.0.tar.gz.

File metadata

  • Download URL: pytest_airflow_in_a_box-0.8.0.tar.gz
  • Upload date:
  • Size: 203.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pytest_airflow_in_a_box-0.8.0.tar.gz
Algorithm Hash digest
SHA256 8c195882f1edc4a876b7e2c1650dce7c3a23972778ac44b544269408e65620e5
MD5 8a229fe8bd50b30a3a06d310dda9148c
BLAKE2b-256 03b83b55f3883491049db68dac5be8892abe847cbed10d2d506804b5ac5b6867

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_airflow_in_a_box-0.8.0.tar.gz:

Publisher: release.yml on nredd/pytest-airflow-in-a-box

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_airflow_in_a_box-0.8.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_airflow_in_a_box-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2bdcea386b0e701e79658980316488a00d14a270f953a04790ce67351aae4d93
MD5 c3f55e6e5e994c6ec50009233581dfa9
BLAKE2b-256 b9f562588956c2f928295f5cb154eade5411bcf486a72f73bc1504028cbbc33a

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_airflow_in_a_box-0.8.0-py3-none-any.whl:

Publisher: release.yml on nredd/pytest-airflow-in-a-box

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.13.1

2 files

0.13.0

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

This release

0.8.0 This release

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page