Testing Jobs¶
Jobs are regular Python classes and can be tested using Django's standard test utilities. However, since Jobs are run through Celery and rely on specific setup (like enabling the Job in the database), Nautobot provides additional helpers to make testing easier and more realistic.
While individual methods within your Job can and should be tested in isolation, you'll likely also want to test the entire execution of the Job.
Using TransactionTestCase
¶
Nautobot provides a subclass of Django's TransactionTestCase
that is tailored for testing Jobs. This test class ensures each test starts with a clean database and supports integration with Nautobot's Job system.
Use nautobot.apps.testing.TransactionTestCase
instead of django.test.TestCase
, especially when invoking full Job execution via run_job_for_testing()
.
Why TransactionTestCase
?
TransactionTestCase
allows database changes to persist across the Celery task boundary when testing, which is required because Jobs are executed in a different process context.
(Refer to the Django documentation if you're interested in the differences between these classes - TransactionTestCase
from Nautobot is a small wrapper around Django's TransactionTestCase
).
When using TransactionTestCase
(whether from Django or from Nautobot) each test runs on a completely empty database. Furthermore, Nautobot requires new Jobs to be enabled before they can run. Therefore, we need to make sure the Job is enabled before each run which run_job_for_testing
handles for us.
Running a Job in a Test¶
Nautobot provides the run_job_for_testing()
utility to simplify test execution. It handles Job registration, enables the Job if needed, and executes it with your provided input variables using the standard Celery worker system.
What run_job_for_testing()
Does
This helper:
- Enables the Job in the database if not already enabled
- Executes it with the provided variables
- Returns a JobResult
object for inspection
A simple example of a Job test case might look like the following:
from nautobot.apps.testing import run_job_for_testing, TransactionTestCase
from nautobot.extras.models import Job, JobLogEntry
class MyJobTestCase(TransactionTestCase):
def test_my_job(self):
# Testing of Job "MyJob" in file "my_job_file.py" in $JOBS_ROOT
job = Job.objects.get(job_class_name="MyJob", module_name="my_job_file")
# or, job = Job.objects.get_for_class_path("local/my_job_file/MyJob")
job_result = run_job_for_testing(job, var1="abc", var2=123)
# Inspect the logs created by running the job
log_entries = JobLogEntry.objects.filter(job_result=job_result)
for log_entry in log_entries:
self.assertEqual(log_entry.message, "...")
The test files should be placed under the tests
folder in the app's directory or under JOBS_ROOT
.
Running Tests
You can run Job tests using either Django's test runner or pytest
, depending on your environment:
nautobot-server test myapp.tests.test_jobs
pytest myapp/tests/test_jobs.py
If your test file lives in JOBS_ROOT
, make sure the JOBS_ROOT
environment variable is set.
Tip
For more advanced examples refer to the Nautobot source code, specifically nautobot/extras/tests/test_jobs.py
.
Debugging Job Performance¶
Debugging the performance of Nautobot Jobs can be tricky, because they are executed in the Celery worker context. In order to gain extra visibility, cProfile can be used to profile the Job execution.
The 'profile' form field on Jobs is automatically available when the DEBUG
settings is True
. When you select that checkbox, a profiling report in the pstats format will be written to the file system of the environment where the Job runs. Normally, this is on the file system of the worker process, but if you are using the nautobot-server runjob
command with --local
, it will end up in the file system of the web application itself. The path of the written file will be logged in the Job.
Note
If you need to run this in an environment where DEBUG
is False
, you have the option of using nautobot-server runjob
with the --profile
flag. According to the docs, cProfile
should have minimal impact on the performance of the Job; still, proceed with caution when using this in a production environment.
Reading Profiling Reports¶
Profiling files are saved in the format expected by Python's pstats
module. A full description on how to deal with the output of cProfile
can be found in the Instant User's Manual. You can analyze them as follows:
import pstats
job_result_uuid = "66b70231-002f-412b-8cc4-1cc9609c2c9b"
stats = pstats.Stats(f"/tmp/nautobot-jobresult-{job_result_uuid}.pstats")
stats.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(10)
This prints the 10 functions where the Job spent the most time. You can customize the sort key and output to focus on specific bottlenecks.