Skip to content

Add Pytest plugin and Django testing connector#1547

Open
Xowap wants to merge 3 commits into
procrastinate-org:mainfrom
Xowap:django_testing
Open

Add Pytest plugin and Django testing connector#1547
Xowap wants to merge 3 commits into
procrastinate-org:mainfrom
Xowap:django_testing

Conversation

@Xowap
Copy link
Copy Markdown
Contributor

@Xowap Xowap commented May 10, 2026

Closes #1546

As described in #1546, one of the great things about Procrastinate is its robustness when interacting directly with the database. However, when testing applications using the standard in-memory connector, this level of database-level interaction often leads to discrepancies and test failures.

This PR addresses that gap by introducing a new testing connector specifically for Django (DjangoTestingConnector). Alongside it, a new Pytest plugin automatically equips Django projects with fixtures for running Procrastinate workers in a test environment. Additionally, it offers seamless compatibility with time-mocking solutions like freezegun, significantly easing the process of testing scheduled and deferred tasks.

Successful PR Checklist:

  • Tests
    • (not applicable?)
  • Documentation
    • (not applicable?)

PR label(s):

Summary by CodeRabbit

  • Documentation

    • Expanded Django integration testing guidance with examples for the new testing connector and pytest plugin fixtures.
  • New Features

    • Added a testing connector for Django integration tests enabling inline job execution within test transactions.
    • Added pytest fixtures (run_procrastinate_jobs, arun_procrastinate_jobs) for executing queued jobs synchronously and asynchronously in tests.
    • Added time travel support for testing scheduled jobs with frozen time simulation.

Review Change Stack

Xowap added 2 commits May 10, 2026 13:21
This introduces `DjangoTestingConnector` to provide a unified testing
environment for Django applications without database side-effects or test
contamination. It simulates PostgreSQL LISTEN/NOTIFY in-memory for
`defer_jobs`, `defer_periodic_job`, and `cancel_job`, avoiding the need
for a real worker during tests.

Additionally, it integrates automatically with `freezegun` by using a custom
schema `_procrastinate_testing` to override PostgreSQL system time functions
(`now()`, `transaction_timestamp()`, etc.) directly on the connection
level. This ensures both application logic and database queries share a
consistent view of time during tests.

Comprehensive tests are included to verify both the notification simulation
and time freezing capabilities, and `freezegun` has been added to the test
dependencies.
Introduces a pytest plugin for Procrastinate that automatically registers
if `pytest` and `django` are available. The plugin provides
`run_procrastinate_jobs` and `arun_procrastinate_jobs` fixtures,
allowing users to easily execute background jobs within tests using the
`DjangoTestingConnector`. This ensures environments without pytest or
django are unaffected while simplifying job processing in tests.

The `DjangoTestingConnector._dictfetch` was also updated to properly
parse JSONB arguments, which avoids crashes when accessing job properties
like `job.task_kwargs.items()` within a Django test context. Finally,
the testing documentation has been updated to showcase how to use the
plugin with tools like `freezegun` and Django's ORM.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete testing infrastructure for Procrastinate in Django projects. A new DjangoTestingConnector runs workers inline within test transactions, simulates PostgreSQL LISTEN/NOTIFY in-memory, and integrates with freezegun for time-travel testing. A Pytest plugin auto-discovers and provides run_procrastinate_jobs and arun_procrastinate_jobs fixtures for synchronous and async job execution, complemented by SQL time-override functions and comprehensive documentation.

Changes

Django Testing Connector and Pytest Plugin

Layer / File(s) Summary
DjangoTestingConnector implementation
procrastinate/contrib/django/testing.py
Subclasses Django connector to intercept queries, emit job-deferred and job-cancelled notifications via in-memory callback, detect frozen time and apply SQL time overrides, and deserialize JSON args fields for test adapter compatibility.
SQL time-override functions
procrastinate/sql/queries.sql
Adds _procrastinate_testing schema with four time functions that return fixed timestamps, enabling deterministic scheduling tests when freezegun freezes time.
Pytest plugin and fixtures
procrastinate/pytest_plugin.py
Exports optional run_procrastinate_jobs and arun_procrastinate_jobs fixtures that inject DjangoTestingConnector, disable signal handling/listen, and run workers inline with customizable options.
Entry point and dependencies
pyproject.toml
Registers pytest plugin via pytest11 entry point for auto-discovery; adds freezegun to test dependencies.
DjangoTestingConnector integration tests
tests/integration/contrib/django/test_testing.py
Verifies notification emission (sync and async paths) and time-override behavior with freezegun; confirms functions are only overridden during frozen contexts.
Pytest plugin fixture integration tests
tests/integration/contrib/django/test_pytest_plugin.py
Tests fixtures with sync/async job deferral, time-travel scheduling via freezegun, and ORM-based job modifications using transaction=True.
Pytest plugin dependency tests
tests/unit/test_pytest_plugin.py
Verifies optional dependency handling: fixtures absent when Django or Pytest missing, present when both available.
Testing documentation
docs/howto/django/tests.md
Documents DjangoTestingConnector and fixtures with sync/async/time-travel examples; updates manual configuration example; corrects spelling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Suggested reviewers

  • ewjoachim

Poem

🐰 A connector springs forth from the garden of tests,
With LISTEN and NOTIFY dancing in memory's nest,
Time freezes and thaws at the fixtures' request,
While jobs hop through transactions—now that's quite blessed! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding both a Pytest plugin and Django testing connector, which are the primary deliverables.
Linked Issues check ✅ Passed All coding requirements from issue #1546 are met: DjangoTestingConnector with in-memory LISTEN/NOTIFY simulation and frozen time support via SQL schema overrides, Pytest plugin with fixtures, freezegun integration, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue objectives: testing connector implementation, pytest plugin setup, SQL time-freezing utilities, test dependencies (freezegun), documentation updates, and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  procrastinate
  __init__.py
  app.py
  blueprints.py
  connector.py
  exceptions.py
  job_context.py
  jobs.py
  manager.py
  metadata.py
  periodic.py
  psycopg_connector.py
  retry.py
  schema.py
  sync_psycopg_connector.py
  tasks.py
  types.py
  utils.py
  procrastinate/contrib/django
  __init__.py
  django_connector.py
  exceptions.py
  procrastinate_app.py
  settings.py
  testing.py 1-38, 44, 50, 65, 79, 90, 99-100, 131, 161-162, 166-167, 177
  utils.py
  procrastinate/sql
  __init__.py
Project Total  

The report is truncated to 25 files out of 27. To see the full report, please visit the workflow summary page.

This report was generated by python-coverage-comment-action

@github-actions github-actions Bot added the PR type: feature ⭐️ Contains new features label May 10, 2026
@Xowap Xowap marked this pull request as ready for review May 15, 2026 09:56
@Xowap Xowap requested a review from a team as a code owner May 15, 2026 09:56
@Xowap
Copy link
Copy Markdown
Contributor Author

Xowap commented May 15, 2026

Marking ready for review as I have been testing this enough in my project to gain confidence in its stability

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
procrastinate/pytest_plugin.py (3)

38-46: 💤 Low value

Clarify "all awaiting jobs" in docstring.

The docstring states the fixture "execute all awaiting Procrastinate jobs", but with wait=False the worker processes jobs until none are immediately available. If jobs spawn other jobs or have complex dependencies, users may need to call the fixture multiple times to process everything.

Consider clarifying: "Runs the Procrastinate worker once to process available jobs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@procrastinate/pytest_plugin.py` around lines 38 - 46, Update the docstring
for the fixture in procrastinate/pytest_plugin.py to clarify that it runs the
worker only once and only processes jobs that are immediately available (i.e.,
uses wait=False), so jobs that spawn additional jobs or have complex
dependencies may require calling the fixture multiple times; replace "execute
all awaiting Procrastinate jobs" with a precise sentence such as "Runs the
Procrastinate worker once to process available jobs (uses wait=False); jobs that
spawn further work may require repeated calls." Reference the fixture's
docstring block to make this edit.

49-54: 💤 Low value

Consider connector reuse within a single test.

The fixture creates a new DjangoTestingConnector() instance on each invocation. This means if a test calls run_procrastinate_jobs() multiple times, each call gets a fresh connector with no shared notification state.

If this is intentional for strong test isolation, consider documenting this behavior. If tests should share connector state across multiple calls, consider creating the connector once per test by moving it outside the inner function f.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@procrastinate/pytest_plugin.py` around lines 49 - 54, The current inner
function f creates a new DjangoTestingConnector() each call so repeated calls in
the same test don't share notification state; to reuse connector state per test,
instantiate DjangoTestingConnector() once outside f (e.g., in the enclosing
fixture scope) and reuse that instance inside f when calling
django_app.replace_connector(...)/django_app.run_worker(...); alternatively, if
isolation is intended, add a short inline comment or update the surrounding
fixture docstring to state that f creates a fresh connector on every invocation.
Ensure you reference the DjangoTestingConnector and the f wrapper that calls
django_app.replace_connector and django_app.run_worker when making the change.

60-66: 💤 Low value

Clarify "all awaiting jobs" in docstring.

Same as the sync fixture: the docstring claims to "execute all awaiting Procrastinate jobs" but wait=False means the worker runs once. Consider rephrasing for precision.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@procrastinate/pytest_plugin.py` around lines 60 - 66, The docstring for the
async fixture (the one described as "Fixture that provides an asynchronous
function to execute all awaiting Procrastinate jobs") is misleading because the
worker is invoked with wait=False and thus runs once rather than continuously;
update the fixture's docstring (and mention run_procrastinate_jobs for parity)
to state that the returned async helper runs the worker a single time to process
jobs currently queued (replacing the app connector with DjangoTestingConnector
for the duration) and does not wait for or process jobs that arrive afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@procrastinate/pytest_plugin.py`:
- Around line 38-46: Update the docstring for the fixture in
procrastinate/pytest_plugin.py to clarify that it runs the worker only once and
only processes jobs that are immediately available (i.e., uses wait=False), so
jobs that spawn additional jobs or have complex dependencies may require calling
the fixture multiple times; replace "execute all awaiting Procrastinate jobs"
with a precise sentence such as "Runs the Procrastinate worker once to process
available jobs (uses wait=False); jobs that spawn further work may require
repeated calls." Reference the fixture's docstring block to make this edit.
- Around line 49-54: The current inner function f creates a new
DjangoTestingConnector() each call so repeated calls in the same test don't
share notification state; to reuse connector state per test, instantiate
DjangoTestingConnector() once outside f (e.g., in the enclosing fixture scope)
and reuse that instance inside f when calling
django_app.replace_connector(...)/django_app.run_worker(...); alternatively, if
isolation is intended, add a short inline comment or update the surrounding
fixture docstring to state that f creates a fresh connector on every invocation.
Ensure you reference the DjangoTestingConnector and the f wrapper that calls
django_app.replace_connector and django_app.run_worker when making the change.
- Around line 60-66: The docstring for the async fixture (the one described as
"Fixture that provides an asynchronous function to execute all awaiting
Procrastinate jobs") is misleading because the worker is invoked with wait=False
and thus runs once rather than continuously; update the fixture's docstring (and
mention run_procrastinate_jobs for parity) to state that the returned async
helper runs the worker a single time to process jobs currently queued (replacing
the app connector with DjangoTestingConnector for the duration) and does not
wait for or process jobs that arrive afterward.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 290db28a-c995-4943-8838-137db21f2b94

📥 Commits

Reviewing files that changed from the base of the PR and between cceb33e and df8c99e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • docs/howto/django/tests.md
  • procrastinate/contrib/django/testing.py
  • procrastinate/pytest_plugin.py
  • procrastinate/sql/queries.sql
  • pyproject.toml
  • tests/integration/contrib/django/test_pytest_plugin.py
  • tests/integration/contrib/django/test_testing.py
  • tests/unit/test_pytest_plugin.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR type: feature ⭐️ Contains new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add Django testing connector and Pytest plugin with time-mocking support

1 participant