Skip to content

[Tech Debt] Improve Unit Test DB Isolation #71577

Description

@ferruzzi

Body

This is a direct follow-up to #68374 with all the same reasoning: We have a lot of unit tests which start by cleaning the DB in the setup phase. This is a clear indication that something is leaking artifacts into the database causing other tests to fail. All unit test fixtures which create a row should own that row and clean it up. A blanket bd cleanup in the unit test's teardown should be an exception when there is no fixture, not the norm. No unit test should require pre-cleaning in setup in order to avoid another test's garbage. As before, I'll get to this eventually, but I'm putting up an Issue in case someone wants to get to it before I do.

After @seanghaeli pointed out other spots we didn't clean up, I set Claude on the task of finding any remaining unit tests that match the pre-cleaning pattern and it found A LOT. The numbers below are unverified Claude output, but should serve as a solid launching point.


  Tables cleared in setup position, by frequency:
  
   91  clear_db_runs               7  clear_db_pools
   53  clear_db_dags               7  clear_db_logs
   26  clear_db_assets             7  clear_db_import_errors
   18  clear_db_connections        6  clear_db_deadline
   17  clear_db_serialized_dags    5  clear_db_backfills
   14  clear_db_jobs               4  clear_db_apdr / pakl / callbacks / triggers
    9  clear_db_variables          3  clear_db_revoked_tokens / xcom
    9  clear_db_dag_bundles        2  clear_db_deadline_alert
    8  clear_db_connection_tests   1  clear_db_dag_warnings / dag_code / task_reschedule
    8  clear_db_teams

Notably, dag_maker.cleanup() has the same gap for assets that it had for bundles before #69093: it deletes AssetEvent but not the AssetModel / AssetActive / AssetAliasModel rows a Dag's schedule creates. Fixing it the same way (track what was created, delete when unreferenced, FK-safe order) should account for the ~26 clear_db_assets pre-cleans on its own.

Proposal

I'm using that term very loosely, these are just Thoughts. Cleaning up the codebase is not very useful without enforcing the change, so some ideas in addition to the actual clean up:

  1. A new enforcer: I don't know if this is a pre-commit or a ruff check or what this actually looks like, but we should be preventing tests from calling db_clean in the setup. If a new test needs that to pass, something is already wrong and we're just masking an existing issue.

  2. A new "leak detector": This might be in the form of a fixture which snapshots the pre- and post-test table row counts and reports if there is any variation? Maybe include it in the existing "this test needs a db" fixture?

  3. Several tests create Job rows by hand and nothing cleans them up; we might want a job_maker fixture akin to dag_maker which tracks those creations and cleans up on teardown? Similarly, it doesn't look like anything owns tracking and cleaning up Connections, Variables, or Pools. maybe a similar pattern can be employed there?

We can either add the enforcer ahead of time with an allowlist of existing cases which we clean as we go, or we need to clean before that lands. I kind of like the former as it doubles as a tracker for remaining work and closes the floodgate so we're not stuck making another pass later to catch the issues that were merged while we were working.

Triage Rules:

How do you tell a deletable site from a load-bearing one? I see three cases:

  • Already covered by dag_maker (it deletes DagRun, TaskInstance, DagVersion, XCom, DagModel, TaskMap, plus its own bundles, and SerializedDagModel / DeadlineAlert cascade off dag_version.id and serialized_dag.id). Delete the pre-clean, unless the test bypasses dag_maker via DagBag, example Dags, or a hand-built DagModel.
  • Genuine fixture gap (assets today). Fix the fixture first, then delete.
  • Nothing owns the table (Job, Log, Pool, Variable, Connection, ConnectionTest, RevokedToken, Backfill). Load-bearing until an owning fixture exists; do not delete yet.

Once we have the framework in place to prevent regression, here is what Claude reports for the current violations that need to be fixed. Included here for a sense of scope only, I have not validated the numbers and naturally this will drift between now and then anyway:

┌───────────────────────────────────────────────────┬───────┬───────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Batch                                             │ Sites │ Files │ Notes                                                                                                                  │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ api_fastapi/execution_api/versions/               │ 42    │ 12    │ Almost entirely clear_db_runs. Most mechanical batch; good starter.                                                    │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ api_fastapi/core_api/routes/ui/                   │ 39    │ 10    │ Includes the apdr / pakl partition tables.                                                                             │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ api_fastapi/core_api/routes/public/               │ 34    │ 14    │ Blocked on Pool / Variable / Connection / Log ownership.                                                               │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ api_fastapi misc (common/, auth/, conftest.py,    │ 10    │ 5     │                                                                                                                        │
│ core_api/test_app.py)                             │       │       │                                                                                                                        │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ models/                                           │ 46    │ 18    │ Blocked on the dag_maker asset fix. Also has one leftover bundle/team site from #68374.                                │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ cli/commands/                                     │ 24    │ 9     │ Heavy on connections and variables; blocked on Connection / Variable ownership.                                        │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ dag_processing/                                   │ 15    │ 3     │ Includes two leftover bundle/team sites from #68374.                                                                   │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ jobs/                                             │ 12    │ 2     │ test_scheduler_job.py + test_triggerer_job.py. Blocked on Job ownership.                                               │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ utils/                                            │ 10    │ 4     │                                                                                                                        │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ core misc: always/, api/, assets/, core/, state/, │ 19    │ 10    │ One combined batch. Includes one leftover team site in ti_deps/.                                                       │
│ ti_deps/, timetables/                             │       │       │                                                                                                                        │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ providers: standard 8, celery 6, snowflake 3,     │ 19    │ 8     │ Version-gated behind AIRFLOW_V_3_0_PLUS in places, so this batch carries provider compat rules the core batches do     │
│ common.io 2                                       │       │       │ not. Also has the last leftover bundle site from #68374.                                                               │
├───────────────────────────────────────────────────┼───────┼───────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Total                                             │ 270   │ 95    │                                                                                                                        │
└───────────────────────────────────────────────────┴───────┴───────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Committer

  • I acknowledge that I am a maintainer/committer of the Apache Airflow project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:metaHigh-level information important to the community

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions