Post

pytest-depends-on: Explicit Test Dependency Management for Pytest

Declare explicit dependencies between tests and automatically skip dependent tests when their prerequisites fail. pytest-depends-on brings proper dependency management to your pytest suite.

pytest-depends-on: Explicit Test Dependency Management for Pytest

In integration and end-to-end testing, tests often depend on each other. If test_login fails, running test_dashboard is pointless — and its failure is misleading. Without explicit dependency management, a single upstream failure cascades into a wall of failures across your suite, making CI reports noisy and root-cause analysis painful. pytest-depends-on lets you declare dependencies explicitly and skip dependent tests automatically when their prerequisites don’t pass.

Why I Built This

I was working on an end-to-end API test suite where each test built on the state created by the previous one — create user, fetch user, update user, delete user. When the create call broke, I’d get four failures in the report instead of one. Debugging meant figuring out which failures were real and which were just cascading noise. I wanted the test runner to be smart enough to stop when it already knows a test can’t possibly succeed.

Installation

1
pip install pytest-depends-on
1
uv add pytest-depends-on

Quick Start

Declare a dependency with @pytest.mark.depends_on, then enable the plugin via pytest.ini:

1
2
3
4
5
6
7
8
import pytest

def test_parent():
    assert True

@pytest.mark.depends_on(tests=["test_parent"])
def test_child():
    assert True
1
2
3
4
5
6
7
[pytest]
addopts =
    --depends-on
    --depends-on-reorder

markers =
    depends_on: mark test as dependent on another test

test_child runs only if test_parent passes. If it fails or is skipped, test_child is automatically skipped. The plugin is opt-in — without --depends-on, all markers are ignored and every test runs normally.

Real-World Example

A typical CRUD API test flow — each operation depends on the previous one succeeding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_create_user():
    response = api.post("/users", data={"name": "Alice"})
    assert response.status_code == 201

@pytest.mark.depends_on(tests=["test_create_user"])
def test_get_user():
    response = api.get("/users/alice")
    assert response.status_code == 200

@pytest.mark.depends_on(tests=["test_get_user"])
def test_update_user():
    response = api.patch("/users/alice", data={"name": "Alice Updated"})
    assert response.status_code == 200

@pytest.mark.depends_on(tests=["test_update_user"])
def test_delete_user():
    response = api.delete("/users/alice")
    assert response.status_code == 204

If test_create_user fails, the remaining three tests are automatically skipped — one failure in the report, clear localization, no noise.

Key Features

--depends-on-reorder solves the collection order problem automatically. If test_child is collected before test_parent, it will always skip even when the parent would have passed. The flag triggers a topological sort of the entire test suite at collection time, guaranteeing parents always run before their dependents regardless of file order or definition order within a file:

1
2
Before reorder:  test_child_a → test_child_b → test_parent
After reorder:   test_parent  → test_child_a → test_child_b

Circular dependencies are detected, logged as a warning, and handled gracefully — the run continues.

Custom status dependencies let you express more nuanced relationships. The default expects the parent to have passed, but you can flip it — for example, to run a cleanup test only when provisioning fails:

1
2
3
4
5
from pytest_depends_on.consts.status import Status

@pytest.mark.depends_on(tests=["test_provision_resource"], status=Status.FAILED)
def test_cleanup_on_failure():
    cleanup_partial_resources()

All five pytest statuses are supported: PASSED, FAILED, SKIPPED, XFAILED, XPASSED.

Soft dependencies via allowed_not_run=True let a dependent test proceed even if its parent hasn’t run yet — useful when you’re running a filtered subset of the suite and don’t want non-relevant dependencies causing skips:

1
2
3
@pytest.mark.depends_on(tests=["test_setup"], allowed_not_run=True)
def test_feature():
    pass

Status tracking is automatic. The plugin hooks into the call phase and records every test outcome — no manual tracking, no shared state to manage.

Goes Well With

  • custom-python-logger — log dependency resolution and test outcomes in a structured way for better CI reporting and debugging
  • pytest-plugins — CI reporting layer that works alongside the requirements manifest in pipeline workflows
  • pytest-collect-requirements — collect test requirements from the same dynamic sources as your parameters for fully data-driven pipelines
  • pytest-dynamic-parameterize — express dependencies between tests that are parameterized from dynamic sources, ensuring proper execution order and skip logic

One real failure. Not ten misleading ones.

This post is licensed under CC BY 4.0 by the author.