# Performance Testing in Odoo 19

> A related field can turn 3 queries into 47 without any test noticing. How assertQueryCount, @warmup and @users give an Odoo module a performance regression gate.

**Date:** 2026-05-04
**Source:** <https://trobz.com/insights/performance-testing-in-odoo/>

---



Imagine that you are tasked to add one `related` field to point from a sale order line to the partner's country. The field is read-only, the code is clean, the unit tests pass. After deploying to production, the sale order list page degrades from 3 queries to 47 because the ORM is now making a per-row database call to fetch the partner record, then the country record, for every line. Odoo's ORM will not warn you and standard test suite will not catch it but your customer will.

Most Odoo Developers often ignore the performance testing layer built into the framework. This article covers `assertQueryCount`, `@warmup`, `@users`, `@tagged`, and a full real-world example from the OCA `fs_image` module.

## `assertQueryCount`

`assertQueryCount(n)` is a context manager on `odoo.tests.common.BaseCase`. Wrap any block in it and the test fails if the SQL count exceeds `n`.

```python
with self.assertQueryCount(42):
    do_something()
```

Under the hood, it reads `cr.sql_log_count` before and after the block, with a flush on both sides to capture deferred ORM writes. The flush matters: Odoo batches some writes, and without it the count before the context manager exits could be understated. The number you assert against is the real number, not an optimistic one.

`assertQueryCount` also accepts per-login keyword arguments for use with the `@users` decorator:

```python
with self.assertQueryCount(admin=3, demo=5):
    do_something()
```

This lets you document and assert different counts for different user types, which is useful when access rights checks or ir.rule evaluation varies by user.

## `@warmup`

The first time any ORM operation runs in a test, it populates caches: field definitions, access rights, ir.rule records. Those cache misses produce extra queries. The count on the first run is inflated and unstable.

`@warmup` solves this by running the test twice. The first pass (`self.warm = False`) runs the full test body, populates caches, then rolls back. The second pass (`self.warm = True`) runs for real with assertions active. `assertQueryCount` silently skips during the warm-up pass, so only the second run produces a failure or success.

```python
@warmup
def test_my_operation(self):
    with self.assertQueryCount(5):
        self.env['sale.order'].browse(ids).action_confirm()
```

Without `@warmup`, a test may pass in development (where caches are already warm from previous test runs or UI navigation) and fail in CI (where each test class starts cold). Or vice versa. Either way, the assertion is measuring noise. `@warmup` is mandatory for any `assertQueryCount` that should be meaningful.

## Putting It Together: OCA `fs_image`

A concrete example is The OCA storage module's `fs_image` test.

```python
@users("__system__")
@warmup
def test_generated_sql_commands(self):
    with self.assertQueryCount(__system__=3):
        instance = self.env["test.image.model"].create(
            {"fs_image": FSImageValue(name=self.filename, value=self.image_w)}
        )
    instance.invalidate_recordset()
    with self.assertQueryCount(__system__=1):
        self.assertEqual(instance.fs_image.getvalue(), self.image_w)
        self.env.flush_all()
```

Line by line:

`@users("__system__")` runs the test as the system user and passes `__system__` as the login key in `assertQueryCount`. Use `@users` when the count depends on which user is active, and use the per-login form of the assertion to match.

`@warmup` ensures caches are populated before counts are measured. Always stack it inside `@users` (innermost decorator runs first).

`assertQueryCount(__system__=3)` asserts the create operation costs exactly 3 queries as the system user. This is the write path assertion.

`instance.invalidate_recordset()` resets the per-record ORM cache between the two assertions. Without this, the read in the second block might hit an in-memory cache rather than the database, understating the real query count.

`assertQueryCount(__system__=1)` asserts the read path costs 1 query. Two separate assertions, write and read, give a precise budget for each operation.

This pattern is directly copyable. Replace the model, the create values, and the threshold numbers with your own, and you have a performance regression gate for any Odoo module.

## Going Further: Profiler

When `assertQueryCount` tells you that counts are too high but not why, use [Profiler](https://www.odoo.com/documentation/19.0/developer/reference/backend/performance.html#profiling). It is a context manager that captures SQL queries, periodic stack traces, and memory snapshots in a single pass.

```python
with self.profile():
    self.env['sale.order'].browse(ids).action_confirm()
```

Results are saved to the database and viewable in Odoo's performance UI. Use `Profiler` when you need to trace which method is generating the extra queries, not just that the total is over budget.

## Conclusion

Performance testing in Odoo is often skipped during development and only discovered to be missing when real users interact with the module in production. By then, the cost is a support ticket, a rollback, and the customer's trust.

`assertQueryCount` plus `@warmup` is the minimum viable safety net. Add `@users` when counts vary by user. Use `Profiler` when you need to trace the root cause.

