Test Procrastinate interactions in a Django project¶
Unit tests¶
There are different kind of tests you might want to do with Procrastinate in a Django project. One such test is a unit test where you want fast tests that don’t hit the database.
For this, you can use the procrastinate.testing.InMemoryConnector
.
Here’s an example pytest fixture you can write in your conftest.py
:
import pytest
from procrastinate import testing
from procrastinate.contrib.django import procrastinate_app
from mypackage.procrastinate import my_task
@pytest.fixture
def app():
in_memory = testing.InMemoryConnector()
# Replace the connector in the current app
# Note that this fixture gives you the app back for convenience, but it's
# the same instance as you'd get with `procrastinate.contrib.django.app`.
with procrastinate_app.current_app.replace_connector(in_memory) as app:
yield app
def test_my_task(app):
# Run the task
my_task.defer(a=1, b=2)
# Access all the existing jobs
jobs = app.connector.jobs
assert len(jobs) == 1
# Run the jobs
app.run_worker(wait=False)
assert task_side_effect() == "done"
# Reset the in-memory pseudo-database. This usually isn't necessary if
# you make small scoped tests as you'll use a new app fixture for each test
# but it might come in handy.
app.connector.reset()
Note
procrastinate.contrib.django.procrastinate_app.current_app
is not exactly
documented but whatever instance this variable points to is what
will be returned as procrastinate.contrib.django.app
. It’s probably a bad
idea to manipulate this outside of tests.
Integration tests¶
Another kind of test, maybe more frequent within Django, would be an
integration test. In this kind of test you would use the database. You can use
Procrastinate normally with procrastinate.contrib.django.app
in your tests.
You can use the procrastinate models to check if the jobs have been created as expected.
from procrastinate.contrib.django.models import ProcrastinateJob
from mypackage.procrastinate import my_task
def test_my_task():
# Run the task
my_task.defer(a=1, b=2)
# Check the job has been created
assert ProcrastinateJob.objects.filter(task_name="my_task").count() == 1
Note
Registering a new task on the django procrastinate app within a test will make this task available even after the test has run. You probably want to avoid this, for example by creating a new app within each of those tests:
from procrastinate.contrib.django import app
def test_my_task():
new_app = procrastinate_app.ProcrastinateApp(app.connector)
@new_app.task
def my_task(a, b):
return a + b
my_task.defer(a=1, b=2)
...
In addition, you can also run a worker in your integration tests. Whether you
use pytest-django
or Django’s TestCase
subclasses, this requires some
additonal configuration.
In order to run the worker, use the syntax outlined here: Running custom Django scripts.
In order for Procrastinate to be able to use
SELECT FOR UPDATE
, useTransactionTestCase
. WithTestCase
, you can’t test code within a transaction withselect_for_update()
. If you usepytest-django
, use the equivalent@pytest.mark.django_db(transaction=True)
.Lastly, there are useful arguments to pass to
run_worker
:wait=False
to exit the worker as soon as all jobs are doneinstall_signal_handlers=False
to avoid messing with signals in your test runnerlisten_notify=False
to avoid running the listen/notify coroutine which is probably not needed in tests
from procrastinate.contrib.django import app
from django.test import TransactionTestCase
from mypackage.procrastinate import my_task
class TestingTaskClass(TransactionTestCase):
def test_task(self):
# Run tasks
my_task.defer(a=1, b=2)
# Start worker
with app.replace_connector(app.connector.get_worker_connector())
app.run_worker(wait=False, install_signal_handlers=False, listen_notify=False)
# Check task has been executed
assert ProcrastinateJob.objects.filter(task_name="my_task").status == "succeeded"
from procrastinate.contrib.django import app
from mypackage.procrastinate import my_task
@pytest.mark.django_db(transaction=True)
def test_task():
# Run tasks
my_task.defer(a=1, b=2)
# Start worker
with app.replace_connector(app.connector.get_worker_connector())
app.run_worker(wait=False, install_signal_handlers=False, listen_notify=False)
# Check task has been executed
assert ProcrastinateJob.objects.filter(task_name="my_task").status == "succeeded"
# Or with a fixture
@pytest.fixture
def worker(transactional_db):
with app.replace_connector(app.connector.get_worker_connector())
def f():
app.run_worker(wait=False, install_signal_handlers=False, listen_notify=False)
return app
yield f
def test_task(worker):
# Run tasks
my_task.defer(a=1, b=2)
# Start worker
worker()
# Check task has been executed
assert ProcrastinateJob.objects.filter(task_name="my_task").status == "succeeded"
Making the models writable in tests¶
If you need to write to the procrastinate models in your tests, a setting is
provided: PROCRASTINATE_READONLY_MODELS
. If set to False
, the models will be
writable. You may want to use the settings
fixture from pytest-django
to
set this setting only for the tests that need it.
@pytest.fixture
def procrastinate_writable_models(settings):
settings.PROCRASTINATE_READONLY_MODELS = False
Warning
This setting is only intended for tests.