Skip to content

# 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.