Skip to main content

Pytest plugin for coordinating the order in which marked tests run.

Project description

pytest-conductor

A pytest plugin that allows you to control the order in which tests run based on their tags (markers) or fixtures. Perfect for coordinating test execution in CI/CD pipelines, managing test dependencies, and optimizing test suite performance.

โœจ Features

  • ๐ŸŽฏ Tag-based Ordering: Order tests by pytest markers (fast โ†’ slow โ†’ integration)
  • ๐Ÿ”ง Fixture-based Ordering: Order tests by the fixtures they use (db โ†’ redis โ†’ cache)
  • โšก Unmatched Test Handling: Control how tests without matching tags/fixtures are handled
  • ๐Ÿšซ Skip Unmatched Tests: Option to skip tests that don't match the order list entirely
  • ๐Ÿ›ก๏ธ Error Validation: Automatic validation of fixture availability for reliable ordering
  • ๐Ÿ“Š Performance Optimized: Minimal overhead with efficient sorting algorithms
  • ๐Ÿ” Comprehensive Testing: Full test suite with unit tests and integration tests
  • ๐ŸŽญ Interactive Demo: Built-in demo showing all features with detailed logging

Installation

pip install pytest-conductor

๐ŸŽญ Quick Demo

See pytest-conductor in action with our interactive demo:

# Clone the repository
git clone https://github.com/your-username/pytest-conductor.git
cd pytest-conductor

# Run the interactive demo
hatch run demo

The demo shows:

  • Tag ordering (fast โ†’ slow โ†’ integration)
  • Fixture ordering (basic_calculator โ†’ advanced_calculator โ†’ sample_data)
  • Unmatched test handling (first, last, none)
  • Error handling for invalid configurations

๐Ÿ“ Example Project

The repository includes a complete example project demonstrating real-world usage:

# Navigate to the example project
cd example

# Install dependencies and pytest-conductor
hatch run pip install -e ../

# Run tests with coordination
hatch run pytest --tag-order fast slow -v
hatch run pytest --fixture-order basic_calculator advanced_calculator --ordering-mode fixture -v

The example project includes:

  • Calculator application with basic and advanced operations
  • Comprehensive test suite with different tags and fixtures
  • Global and local fixtures demonstrating scope validation
  • Integration tests showing all plugin features

Usage

Ordering Modes

The plugin supports two ordering modes:

  1. Mark Mode (default): Order tests by their pytest markers/tags
  2. Fixture Mode: Order tests by the fixtures they use

Mark Mode - Basic Tag Ordering

Use the --tag-order option to specify the order in which tags should run:

pytest --tag-order fast slow integration

This will run all tests with the fast tag first, then slow tests, then integration tests.

Fixture Mode - Basic Fixture Ordering

Use the --fixture-order option to specify the order in which fixtures should run:

pytest --fixture-order db redis cache --ordering-mode fixture

This will run all tests that use the db fixture first, then tests using redis, then tests using cache.

โš ๏ธ Important Limitation: Fixture ordering only works with fixtures that are globally available to all tests. The plugin will throw an error if you try to order by a fixture that is not available to all tests in your test suite. This ensures reliable ordering behavior.

Error Handling and Validation

The plugin includes robust error handling to prevent configuration issues:

Fixture Availability Validation

When using fixture ordering, the plugin validates that all specified fixtures are available to all tests:

# This will fail if 'nonexistent_fixture' is not available to all tests
pytest --fixture-order nonexistent_fixture --ordering-mode fixture

Error Message: ValueError: Fixtures not available to all tests: nonexistent_fixture. Fixture ordering requires all fixtures to be globally available. Make sure these fixtures are defined in a conftest.py file that is accessible to all tests, or use mark ordering instead.

Best Practices

  • Use global fixtures defined in a root-level conftest.py file
  • Use mark ordering for tests with local or conditional fixtures
  • Test your configuration with --collect-only to catch issues early

Handling Unmatched Tests

Use the --unmatched-order option to control how tests without matching tags/fixtures are handled:

# Run unmatched tests first
pytest --tag-order fast slow --unmatched-order first
pytest --fixture-order db redis --ordering-mode fixture --unmatched-order first

# Run unmatched tests last
pytest --tag-order fast slow --unmatched-order last
pytest --fixture-order db redis --ordering-mode fixture --unmatched-order last

# Run unmatched tests in any order (default)
pytest --tag-order fast slow --unmatched-order any
pytest --fixture-order db redis --ordering-mode fixture --unmatched-order any

# Skip unmatched tests entirely
pytest --tag-order fast slow --unmatched-order none
pytest --fixture-order db redis --ordering-mode fixture --unmatched-order none

New in v0.1.0: Skip Unmatched Tests

The --unmatched-order none option is a new feature that allows you to skip tests that don't match your specified order list entirely. This is useful when you want to run only a specific subset of tests:

# Run only fast and slow tests, skip all others
pytest --tag-order fast slow --unmatched-order none

# Run only tests using specific fixtures, skip all others  
pytest --fixture-order db redis --ordering-mode fixture --unmatched-order none

Example Test Structure

Mark Mode Example

import pytest

@pytest.mark.fast
def test_fast_operation():
    """This test will run first when using --tag-order fast slow"""
    assert True

@pytest.mark.slow
def test_slow_operation():
    """This test will run second when using --tag-order fast slow"""
    assert True

def test_no_tags():
    """This test has no tags - behavior depends on --unmatched-order"""
    assert True

@pytest.mark.fast
@pytest.mark.slow
def test_multiple_tags():
    """This test has multiple tags - uses the first one in the order"""
    assert True

Fixture Mode Example

import pytest

@pytest.fixture
def db():
    """Database fixture."""
    return {"type": "database"}

@pytest.fixture
def redis():
    """Redis fixture."""
    return {"type": "redis"}

def test_db_operation(db):
    """This test will run first when using --fixture-order db redis"""
    assert db["type"] == "database"

def test_redis_operation(redis):
    """This test will run second when using --fixture-order db redis"""
    assert redis["type"] == "redis"

def test_no_fixtures():
    """This test has no fixtures - behavior depends on --unmatched-order"""
    assert True

def test_multiple_fixtures(db, redis):
    """This test has multiple fixtures - uses the first one in the order"""
    assert db["type"] == "database"
    assert redis["type"] == "redis"

Command Line Options

  • --tag-order TAG1 TAG2 ...: Specify the order of tags for test execution (mark mode)
  • --fixture-order FIXTURE1 FIXTURE2 ...: Specify the order of fixtures for test execution (fixture mode)
  • --ordering-mode {mark,fixture}: Choose ordering mode (default: mark)
  • --unmatched-order {any,first,last,none}: How to handle tests without matching tags/fixtures
    • any: Run unmatched tests in any order (default)
    • first: Run unmatched tests before tagged/fixture tests
    • last: Run unmatched tests after tagged/fixture tests
    • none: Skip unmatched tests entirely

How It Works

Mark Mode

  1. The plugin extracts tags from test markers (pytest.mark)
  2. Tests are sorted based on the specified tag order
  3. Tests with multiple tags use the highest priority tag (first in the order)
  4. Tests without tags are handled according to the --unmatched-order setting

Fixture Mode

  1. The plugin extracts fixture names from test function parameters
  2. Tests are sorted based on the specified fixture order
  3. Tests with multiple fixtures use the highest priority fixture (first in the order)
  4. Tests without fixtures are handled according to the --unmatched-order setting

Edge Cases and Special Behavior

Multiple Tags/Fixtures

When a test has multiple tags or fixtures that are in your specified order, the plugin will:

  • Run the test only once (no duplication)
  • Use the highest priority tag/fixture (the one that appears first in your order list)

Example with Multiple Tags

@pytest.mark.fast
@pytest.mark.slow
def test_multiple_tags():
    """This test has both 'fast' and 'slow' tags."""
    assert True

When running pytest --tag-order fast slow integration, this test will:

  • Run once (not twice)
  • Run in the fast group (since 'fast' comes first in the order)

Example with Multiple Fixtures

def test_multiple_fixtures(db, redis, cache):
    """This test uses multiple fixtures."""
    assert True

When running pytest --fixture-order db redis cache --ordering-mode fixture, this test will:

  • Run once (not multiple times)
  • Run in the db group (since 'db' comes first in the order)

Conftest Fixtures

The plugin handles fixtures defined in conftest.py files the same way as regular fixtures:

  • Global conftest fixtures (in root conftest.py) are detected normally
  • Nested conftest fixtures (in subdirectory conftest.py files) are also detected
  • The plugin looks at the test function's parameter names, regardless of where the fixture is defined

โš ๏ธ Fixture Availability Requirement: For fixture ordering to work correctly, all fixtures in your --fixture-order list must be available to all tests that might use them. The plugin will throw an error if any fixture in your order list is not available to all tests, ensuring reliable ordering behavior.

Example with Nested Conftest

tests/
โ”œโ”€โ”€ conftest.py          # global_fixture
โ”œโ”€โ”€ unit/
โ”‚   โ”œโ”€โ”€ conftest.py      # unit_fixture
โ”‚   โ””โ”€โ”€ test_unit.py     # uses both global_fixture and unit_fixture
โ””โ”€โ”€ integration/
    โ”œโ”€โ”€ conftest.py      # integration_fixture
    โ””โ”€โ”€ test_integration.py  # uses global_fixture and integration_fixture

When running pytest --fixture-order global_fixture unit_fixture integration_fixture --ordering-mode fixture:

  • Tests in unit/ will run first if they use global_fixture or unit_fixture
  • Tests in integration/ will run first if they use global_fixture or integration_fixture
  • The plugin doesn't need to know where the fixture is defined - it just looks at the test parameters

Handling Deeply Nested Conftest Fixtures

For deeply nested directory structures, the plugin works seamlessly:

tests/
โ”œโ”€โ”€ conftest.py                    # global_fixture
โ”œโ”€โ”€ api/
โ”‚   โ”œโ”€โ”€ conftest.py               # api_fixture
โ”‚   โ”œโ”€โ”€ v1/
โ”‚   โ”‚   โ”œโ”€โ”€ conftest.py           # v1_fixture
โ”‚   โ”‚   โ””โ”€โ”€ test_endpoints.py     # uses global_fixture, api_fixture, v1_fixture
โ”‚   โ””โ”€โ”€ v2/
โ”‚       โ”œโ”€โ”€ conftest.py           # v2_fixture
โ”‚       โ””โ”€โ”€ test_endpoints.py     # uses global_fixture, api_fixture, v2_fixture
โ””โ”€โ”€ database/
    โ”œโ”€โ”€ conftest.py               # db_fixture
    โ”œโ”€โ”€ mysql/
    โ”‚   โ”œโ”€โ”€ conftest.py           # mysql_fixture
    โ”‚   โ””โ”€โ”€ test_queries.py       # uses global_fixture, db_fixture, mysql_fixture
    โ””โ”€โ”€ postgresql/
        โ”œโ”€โ”€ conftest.py           # postgres_fixture
        โ””โ”€โ”€ test_queries.py       # uses global_fixture, db_fixture, postgres_fixture

Key Points:

  1. No special configuration needed - the plugin automatically detects all fixtures used by tests
  2. Fixture scope doesn't matter - whether fixtures are session, module, class, or function scope
  3. Conftest inheritance works - fixtures from parent directories are available to child tests
  4. Ordering is based on test parameters - not fixture definitions

Best Practices for Complex Fixture Structures:

  • Use descriptive fixture names that indicate their purpose (e.g., mysql_db, redis_cache)
  • Consider using fixture prefixes to group related fixtures (e.g., api_v1_client, api_v2_client)
  • When ordering by fixtures, list them in the order you want tests to run

Unmatched Tests

Tests that don't have any of the specified tags or fixtures are handled according to the --unmatched-order setting:

  • any (default): Run unmatched tests in any order
  • first: Run unmatched tests before all tagged/fixture tests
  • last: Run unmatched tests after all tagged/fixture tests
  • none: Skip unmatched tests entirely (they won't run)

Test Execution Order Guarantees

The plugin guarantees that:

  1. No test runs twice - even with multiple matching tags/fixtures
  2. Tests run in the specified order - within each priority group
  3. Unmatched tests are handled predictably - based on your --unmatched-order setting
  4. Fixture dependencies are respected - pytest's own fixture ordering still applies

Examples

Mark Mode Examples

Run fast tests first, then slow tests

pytest --tag-order fast slow

Run unit tests first, then integration tests, with untagged tests last

pytest --tag-order unit integration --unmatched-order last

Run smoke tests first, then full test suite

pytest --tag-order smoke full --unmatched-order last

Fixture Mode Examples

Run database tests first, then cache tests

pytest --fixture-order db cache --ordering-mode fixture

Run API tests first, then database tests, with tests without fixtures last

pytest --fixture-order api db --ordering-mode fixture --unmatched-order last

Run tests with expensive fixtures last

pytest --fixture-order simple expensive --ordering-mode fixture --unmatched-order first

Testing Edge Cases

The plugin includes comprehensive tests for edge cases. To verify the behavior:

Run Edge Case Tests

# Run the edge case test suite
pytest src/unit_tests/test_edge_cases.py -v

# Run practical examples
pytest src/unit_tests/test_edge_case_examples.py -v

Test Multiple Tags Ordering

# Test that tests with multiple tags run only once
pytest src/unit_tests/test_edge_case_examples.py --tag-order fast slow integration -v

Test Multiple Fixtures Ordering

# Test that tests with multiple fixtures run only once
pytest src/unit_tests/test_edge_case_examples.py --fixture-order db redis cache --ordering-mode fixture -v

Test Unmatched Test Handling

# Test unmatched tests running first
pytest src/unit_tests/test_edge_case_examples.py --tag-order fast slow --unmatched-order first -v

# Test unmatched tests running last
pytest src/unit_tests/test_edge_case_examples.py --tag-order fast slow --unmatched-order last -v

๐Ÿ› ๏ธ Development

Installation

# Clone the repository
git clone https://github.com/your-username/pytest-conductor.git
cd pytest-conductor

# Install in development mode
pip install -e .

Test Structure

The project uses a comprehensive test structure:

src/
โ”œโ”€โ”€ pytest_conductor/          # Main plugin code
โ”œโ”€โ”€ unit_tests/               # Unit tests for the plugin itself
โ”‚   โ”œโ”€โ”€ test_tag_ordering.py
โ”‚   โ”œโ”€โ”€ test_fixture_ordering.py
โ”‚   โ”œโ”€โ”€ test_fixture_validation.py
โ”‚   โ”œโ”€โ”€ test_unmatched_none.py
โ”‚   โ””โ”€โ”€ ... (8 test files total)
โ””โ”€โ”€ integration_tests/        # Integration tests using the example project
    โ”œโ”€โ”€ test_pytest_conductor_integration.py
    โ””โ”€โ”€ README.md

Running Tests

# Run unit tests only (test the plugin's core functionality)
hatch run unit-tests

# Run integration tests only (test with real-world example project)
hatch run integration-tests

# Run all tests
hatch run unit-tests && hatch run integration-tests

# Run the interactive demo (shows test coordination with detailed logging)
hatch run demo

Example Project Testing

# Navigate to example project
cd example

# Install pytest-conductor in the example environment
hatch run pip install -e ../

# Run example tests with coordination
hatch run pytest --tag-order fast slow -v
hatch run pytest --fixture-order basic_calculator advanced_calculator --ordering-mode fixture -v

๐Ÿ“‹ Project Structure

pytest-conductor/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ pytest_conductor/          # Main plugin code
โ”‚   โ”œโ”€โ”€ unit_tests/               # Unit tests for the plugin
โ”‚   โ””โ”€โ”€ integration_tests/        # Integration tests with example project
โ”œโ”€โ”€ example/                      # Complete example project
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”œโ”€โ”€ calculator/           # Example application
โ”‚   โ”‚   โ””โ”€โ”€ tests/               # Example tests with various tags/fixtures
โ”‚   โ””โ”€โ”€ pyproject.toml           # Example project configuration
โ”œโ”€โ”€ pyproject.toml               # Main project configuration
โ””โ”€โ”€ README.md                    # This file

Marker Registration

To avoid warnings about unknown markers, you can register your custom markers in your pyproject.toml or pytest.ini file:

[tool.pytest.ini_options]
markers = [
    "fast: marks tests as fast",
    "slow: marks tests as slow", 
    "integration: marks tests as integration tests",
    "unit: marks tests as unit tests",
]

Or in pytest.ini:

[tool:pytest]
markers =
    fast: marks tests as fast
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests

Project details


Download files

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

Source Distribution

pytest_conductor-0.1.0.tar.gz (30.1 kB view details)

Uploaded Source

Built Distribution

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

pytest_conductor-0.1.0-py3-none-any.whl (10.0 kB view details)

Uploaded Python 3

File details

Details for the file pytest_conductor-0.1.0.tar.gz.

File metadata

  • Download URL: pytest_conductor-0.1.0.tar.gz
  • Upload date:
  • Size: 30.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.8

File hashes

Hashes for pytest_conductor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0d39442baddd2225b67c9c38b370f505f5f6cbca49aca1ae9e1a1e7868d1c35b
MD5 7c6172cd86334470a2fc32d077c5120f
BLAKE2b-256 176bdd124e93116ded12b50690529332e9b11640559293ab77090663c35206c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_conductor-0.1.0.tar.gz:

Publisher: release.yml on kassett/pytest-conductor

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_conductor-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_conductor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ba71d5fbd6bacd3e3624528cd2601aa1803fe46953349df2e3f22e2f104b99d0
MD5 dc82b74a8fd48331d9e00954349bb81e
BLAKE2b-256 5244e6367323efc25029ad30d0869d4730b2cd19a90a358e370431f17812d6ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_conductor-0.1.0-py3-none-any.whl:

Publisher: release.yml on kassett/pytest-conductor

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page