While developing custom modules in Odoo, most of the attention usually goes into getting features to work correctly. But as the project grows, things change, new requirements come in, logic gets refactored, and Odoo itself gets upgraded. This is where unexpected issues start appearing.
Unit testing helps prevent this. Instead of manually checking every feature after a change, tests validate the core logic automatically. Odoo 19 provides a built-in testing framework that allows developers to test business logic in a controlled environment using real ORM behavior.
This article explains how unit tests work in Odoo 19, how to write them properly, and how to use tools like tagged and users to make tests more realistic.
What Are Unit Tests in Odoo?
A unit test verifies a small piece of logic in isolation. In Odoo, this typically includes:
- Model methods
- Computed fields
- Constraints
- Access rules
- Business actions
Odoo’s test framework loads a temporary database, executes the logic, and rolls everything back after each test. This means tests do not affect real data and can be run repeatedly.
Test Types Supported by Odoo
Odoo supports multiple test levels:
- Unit tests – Focus on individual methods or rules
- Integration tests – Validate interactions between models
- UI (tour) tests – Simulate frontend behavior
This blog focuses on unit tests, which form the base of a reliable codebase.
Test Directory Structure
All tests must be placed inside the tests folder of a module.
custom_module/
+-- models/
+-- views/
+-- __manifest__.py
+-- tests/
+-- __init__.py
+-- test.py
Make sure the test files are imported in tests/__init__.py.
The most commonly used base class is:
from odoo.tests.common import TransactionCase
TransactionCase runs each test inside a database transaction and rolls it back after execution. This keeps the database clean and avoids data conflicts.
Other available base classes include SavepointCase, but TransactionCase is preferred for most unit tests.
Writing a Basic Unit Test
Let’s assume we extend the Sale Order model with a custom discount rule.
Business Logic Example
class SaleOrder(models.Model):
_inherit = 'sale.order'
def get_discount_rate(self):
for order in self:
if order.amount_total > 10000:
return 10
return 0
Creating the Test Case
Test File: test_sale_order.py
from odoo.tests.common import TransactionCase
class TestSaleOrderDiscount(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env['res.partner'].create({
'name': 'Test Customer'
})
self.product = self.env['product.product'].create({
'name': 'Test Product',
'list_price': 5000,
})
The setUp method prepares test data and runs before every test.
Testing Business Logic
def test_discount_applied(self):
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 3,
'price_unit': 5000,
})]
})
discount = order.get_discount_rate()
self.assertEqual(discount, 10)
This verifies that the discount logic behaves as expected.
Testing Edge Conditions
def test_no_discount_for_small_amount(self):
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [(0, 0, {
'product_id': self.product.id,
'product_uom_qty': 1,
'price_unit': 3000,
})]
})
discount = order.get_discount_rate()
self.assertEqual(discount, 0)
Edge cases are just as important as standard flows.
Using tagged in Unit Tests
What Is tagged?
tagged is a decorator that controls when a test runs. Some tests should not run during module installation, especially if they depend on fully loaded data.
from odoo.tests.common import tagged
Example: Post-Installation Test
from odoo.tests.common import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestInvoicePosting(TransactionCase):
def test_invoice_posting(self):
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.env.ref('base.res_partner_1').id,
'invoice_line_ids': [(0, 0, {
'name': 'Service',
'quantity': 1,
'price_unit': 1500,
})],
})
invoice.action_post()
self.assertEqual(invoice.state, 'posted')
Explanation:
- post_install > run after installation
- at_install > skip during installation
This avoids failures caused by incomplete setup.
Custom Tags
@tagged('sale', 'discount')
class TestSaleDiscount(TransactionCase):Custom tags help organize large test suites and filter them when needed.
Using users in Unit Tests
What Is users?
By default, tests run as Administrator, which bypasses access rules. The users decorator runs tests under a specific user’s permissions.
from odoo.tests.common import users
Example: Testing Access Rights
from odoo.tests.common import TransactionCase, users
from odoo.exceptions import AccessError
class TestSaleOrderSecurity(TransactionCase):
@users('salesman')
def test_salesman_can_create_order(self):
order = self.env['sale.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id
})
self.assertTrue(order)
This test runs with salesman-level access, not admin access.
Example: Blocking Unauthorized Access
@users('user')
def test_normal_user_cannot_confirm_order(self):
order = self.env['sale.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id
})
with self.assertRaises(AccessError):
order.action_confirm()This ensures record rules and access rights are working as intended.
Testing Constraints
from odoo.exceptions import ValidationError
def test_negative_quantity_not_allowed(self):
with self.assertRaises(ValidationError):
self.env['sale.order.line'].create({
'product_id': self.product.id,
'product_uom_qty': -2,
'price_unit': 1000,
})
This confirms that invalid data is correctly rejected.
Running Unit Tests in Odoo 19
To run all tests:
./odoo-bin -d test_db --test-enable --stop-after-init
To run tests for a specific module:
./odoo-bin -d test_db -i custom_module --test-enable --stop-after-init
Test results appear clearly in the logs.
Best Practices for Unit Testing
- Keep each test focused on one behavior
- Avoid relying on demo data
- Use users to validate security
- Use tagged to control execution timing
- Name tests clearly so failures are easy to understand
Unit testing in Odoo 19 is not an optional extra; it’s a practical way to protect business logic from breaking over time. With Odoo’s built-in testing tools, developers can validate behavior, enforce security rules, and catch issues early in the development cycle.
Using tools like tagged and users, tests can closely reflect real usage scenarios instead of ideal admin-only cases. This results in modules that are easier to maintain, safer to upgrade, and more reliable in production environments.
To read more about How to Write a Test Case in Odoo 18 ERP, refer to our blog How to Write a Test Case in Odoo 18 ERP.