- Test file structure and
TransactionCase - Setting up test data with
setUpClass() - Tagging tests with
@tagged - Testing computed fields, constraints, and exceptions
- Running tests with
odoo-bin
Test File Structure
Place tests in a tests/ folder inside your module. Each file must start with test_. Import it in tests/__init__.py:
# tests/__init__.py
from . import test_library_book
from . import test_library_loan
# tests/test_library_book.py
from odoo.tests import TransactionCase, tagged
@tagged('library', '-standard')
class TestLibraryBook(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create test data once for all test methods in this class
cls.book = cls.env['library.book'].create({
'name': 'Clean Code',
'author': 'Robert C. Martin',
'available_copies': 3,
})
cls.member = cls.env['res.partner'].create({
'name': 'Alice Test',
'email': 'alice@example.com',
})
def test_book_created(self):
self.assertEqual(self.book.name, 'Clean Code')
self.assertEqual(self.book.available_copies, 3)
def test_book_checkout(self):
self.book.checkout(self.member)
self.assertEqual(self.book.available_copies, 2)
TransactionCase vs BaseCase
| Class | Isolation | Use when |
|---|---|---|
| TransactionCase | Each test method runs in a savepoint (rolled back after) | Most model tests — creates/writes real records, all rolled back |
| SavepointCase (deprecated) | Like TransactionCase, older API | Avoid in new code |
| HttpCase | Full HTTP server — uses Werkzeug test client | Testing website routes and controller responses |
In TransactionCase, setUpClass() runs once per class in a transaction that is rolled back after all tests complete. Individual test methods each run in a savepoint that is rolled back at the end of the method — so they always start with the data from setUpClass().
Testing Computed Fields
def test_loan_total_computed(self):
order = self.env['library.loan'].create({
'member_id': self.member.id,
'book_id': self.book.id,
})
# Computed fields update when dependencies change
self.book.write({'price': 10.0})
order.invalidate_recordset() # force recompute if stored=False
# For stored computed fields, write a dependency and check
self.assertEqual(order.total_cost, 10.0)
Testing Constraints
from odoo.exceptions import ValidationError, UserError
def test_negative_copies_raises(self):
with self.assertRaises(ValidationError):
self.book.write({'available_copies': -1})
def test_duplicate_isbn_raises(self):
with self.assertRaises(Exception): # IntegrityError or ValidationError
self.env['library.book'].create({
'name': 'Another Book',
'isbn': self.book.isbn, # duplicate unique field
})
def test_checkout_unavailable_raises(self):
self.book.write({'available_copies': 0})
with self.assertRaises(UserError):
self.book.checkout(self.member)
Tagging Tests
The @tagged decorator lets you run specific subsets of tests:
from odoo.tests import tagged
@tagged('library', 'post_install', '-standard')
class TestLibraryLoan(TransactionCase):
...
# Run only tests tagged 'library':
# odoo-bin -t --test-tags library
# Run post_install tests (after module install):
# odoo-bin -t --test-tags post_install
# -standard means: exclude from the default test suite
# Remove -standard to run with default suite
Running Tests
# Run all tests for your module (on test database)
python odoo-bin -t -d test_db --addons-path=addons -u my_module
# Run specific test class
python odoo-bin -t -d test_db --test-tags '/my_module/TestLibraryBook'
# Run with specific tag
python odoo-bin -t -d test_db --test-tags library
# Run tests, log level for SQL debug
python odoo-bin -t -d test_db --log-sql -u my_module
- Place tests in
tests/test_*.py; import them intests/__init__.py - Use
setUpClass()for data shared across all test methods in a class - Each test method starts fresh from
setUpClass()data — rollback is automatic - Use
assertRaises(ValidationError)to test constraints and exceptions
Frequently Asked Questions
Does each test run in its own database?
No — all tests in a test run share the same database. Isolation is achieved via savepoints (SQL SAVEPOINT / ROLLBACK TO SAVEPOINT). The database is created once (or reused) and all test data is created and rolled back per test method. This is why tests are faster than creating a fresh database per test.
How do I test an HTTP controller (website route)?
Inherit from HttpCase instead of TransactionCase. It starts a Werkzeug test client pointing at your Odoo server. Use self.url_open('/your/route') to make requests and assert on the response status code and content. Authenticate with self.authenticate('user', 'password') before making requests that require login.
Should I use demo data for tests?
No — tests should create their own data in setUpClass() or setUp(). Demo data is not guaranteed to be present in all test environments. Tests that depend on demo data are fragile and can break when demo data changes. Always create your own isolated, minimal test fixtures.