Skip to main content

A pytest fixture for high-precision SQL testing in SQLAlchemy.

Project description

pytest-capquery

Build Status Python Version License

Testing your business logic is good, but testing your database interactions is critical. pytest-capquery treats your SQL queries as first-class citizens in your test suite. By asserting the exact queries executed, you create living documentation of what is truly happening behind the scenes. This guarantees deterministic performance, catches N+1 regressions instantly, and ensures your application behaves exactly as intended.

pytest-capquery is a strict, strongly-typed SQLAlchemy pytest plugin designed to enforce exact chronological query execution. It asserts precise SQL strings, parameter bindings, and transaction boundaries (BEGIN, COMMIT, ROLLBACK).

Used By

pytest-capquery is actively used to protect the database performance of:

Installation

Install via pip:

pip install pytest-capquery

Quick Start

The capquery fixture captures all SQLAlchemy statements executed by your code. You can use it to assert precise SQL queries, exact bound parameters, and transaction events in deterministic order.

Here is how you can use the capquery fixture alongside your SQLAlchemy models (e.g., AlarmPanel and Sensor):

from sqlalchemy.orm import Session
from tests.models import AlarmPanel, Sensor

def test_insert_alarm_panel(db_session: Session, capquery):
    # Setup
    panel = AlarmPanel(mac_address="00:11:22:33:44:55", is_online=True)
    sensor = Sensor(name="Front Door", sensor_type="Contact")
    panel.sensors.append(sensor)

    db_session.add(panel)

    # Clear any unrelated previous queries
    capquery.statements.clear()

    # Trigger database flush
    db_session.flush()

    # Assert exact chronological execution, parameters, and transaction boundaries
    capquery.assert_executed_queries(
        "BEGIN",
        (
            # language=SQL
            """
            INSERT INTO alarm_panels (mac_address, is_online)
            VALUES (?, ?)
            """,
            ("00:11:22:33:44:55", 1)
        ),
        (
            # language=SQL
            """
            INSERT INTO sensors (panel_id, name, sensor_type)
            VALUES (?, ?, ?)
            """,
            (1, "Front Door", "Contact")
        )
    )

The N+1 Problem Showcase

One of the most powerful use cases for pytest-capquery is catching performance regressions associated with the 1+N lazy-loading problem. It clearly contrasts inefficient DB loops with optimized joined-loading.

Catching a 1+N Lazy-Loading Regression

If a developer drops the joinedload behavior, pytest-capquery will expose the exact 1+N lazy-loading queries:

def test_demonstrate_n_plus_one_problem(db_session: Session, capquery):
    capquery.statements.clear()

    # Query all panels WITHOUT eagerly loading sensors
    panels = db_session.query(AlarmPanel).all()

    # Accessing the lazy relationship triggers N+1 queries
    for panel in panels:
        _ = panel.sensors

    # Asserting the resulting N+1 problem
    capquery.assert_executed_queries(
        # The 1 Query
        # language=SQL
        """
        SELECT
            alarm_panels.id AS alarm_panels_id,
            alarm_panels.mac_address AS alarm_panels_mac_address,
            alarm_panels.is_online AS alarm_panels_is_online
        FROM alarm_panels
        """,
        # The +N Queries
        (
            # language=SQL
            """
            SELECT
                sensors.id AS sensors_id,
                sensors.panel_id AS sensors_panel_id,
                sensors.name AS sensors_name,
                sensors.sensor_type AS sensors_sensor_type
            FROM sensors
            WHERE ? = sensors.panel_id
            """,
            (1,)
        ),
        (
            # language=SQL
            """
            SELECT
                sensors.id AS sensors_id,
                sensors.panel_id AS sensors_panel_id,
                sensors.name AS sensors_name,
                sensors.sensor_type AS sensors_sensor_type
            FROM sensors
            WHERE ? = sensors.panel_id
            """,
            (2,)
        ),
        (
            # language=SQL
            """
            SELECT
                sensors.id AS sensors_id,
                sensors.panel_id AS sensors_panel_id,
                sensors.name AS sensors_name,
                sensors.sensor_type AS sensors_sensor_type
            FROM sensors
            WHERE ? = sensors.panel_id
            """,
            (3,)
        )
    )

Fixing the N+1 problem with joined-loading

When developers optimize their query with joinedload, pytest-capquery verifies the problem is fixed:

from sqlalchemy.orm import joinedload

def test_avoid_n_plus_one_queries(db_session: Session, capquery):
    capquery.statements.clear()

    # Query WITH eager loading
    panels = db_session.query(AlarmPanel).options(joinedload(AlarmPanel.sensors)).all()

    # Accessing the relationship no longer triggers additional queries
    for panel in panels:
        _ = panel.sensors

    # Asserting only a single JOIN query was executed
    capquery.assert_executed_queries(
        # language=SQL
        """
        SELECT
            alarm_panels.id AS alarm_panels_id,
            alarm_panels.mac_address AS alarm_panels_mac_address,
            alarm_panels.is_online AS alarm_panels_is_online,
            sensors_1.id AS sensors_1_id,
            sensors_1.panel_id AS sensors_1_panel_id,
            sensors_1.name AS sensors_1_name,
            sensors_1.sensor_type AS sensors_1_sensor_type
        FROM alarm_panels
        LEFT OUTER JOIN sensors AS sensors_1 ON alarm_panels.id = sensors_1.panel_id
        """
    )

Contributing

We welcome contributions to make this plugin even better! To ensure a smooth process, please follow these steps:

  1. Open an Issue: Before writing any code, please open an issue to discuss the feature, enhancement, or bug fix you have in mind.
  2. Contribute the Code: Once discussed, fork the repository, create your branch, make your changes, and submit a Pull Request.
  3. Review & Release: Felipe will review your PR. Once approved and merged, the release process will be managed by the maintainer.

Developer Setup

To get your local environment ready for contribution, run the following commands:

# Clone the repository
git clone [https://github.com/fmartins/pytest-capquery.git](https://github.com/fmartins/pytest-capquery.git)
cd pytest-capquery

# Install Python, dependencies, and pre-commit hooks
make setup

# Run the full test suite (requires Docker for E2E multi-dialect tests)
make test

License

This project is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).

Author: Felipe Cardoso Martins

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_capquery-0.1.0.tar.gz (13.2 kB view details)

Uploaded Source

Built Distribution

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

pytest_capquery-0.1.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pytest_capquery-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7e68ee91f3519e6fc3f0ee36ba774f1b21131f8b67a8e901b738226e010b6c20
MD5 0b36a131bd4e7e464f66c85f5e93c95e
BLAKE2b-256 d6af360d0c942c64068d7dcece805dab3ddd984c2d10d05ec1645ba0591c3f66

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on fmartins/pytest-capquery

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

File metadata

File hashes

Hashes for pytest_capquery-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 792a9f5991d98925442d33bf30983bff6aa917c060e053a0b94875340b840b24
MD5 794ba566c9b3a53933d7d8aa7cae7f32
BLAKE2b-256 45d285b7dcde1819e5f570a92bdea5b61f89dae78aad6e756198387f2dd04df5

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on fmartins/pytest-capquery

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