Testing Odoo: TransactionCase Rolls Everything Back
Odoo tests run against a real database, but you don't want them leaving junk behind. TransactionCase solves this: each test runs inside a transaction that is rolled back automatically when the test ends — so tests never pollute the database or each other.
from odoo.tests.common import TransactionCase
class TestCourse(TransactionCase):
def setUp(self):
super().setUp()
self.course = self.env["school.course"].create({"name": "Math"})
def test_seats_default(self):
self.assertEqual(self.course.seats, 20)
def test_negative_seats_blocked(self):
with self.assertRaises(ValidationError):
self.course.seats = -5 # constraint should reject this
| Test class | For |
|---|---|
TransactionCase | model/business logic (rolled back) |
HttpCase | controllers & JS/tours (real HTTP) |
How to run: tests live in a tests/ folder and are picked up when you install/upgrade the module with --test-enable. Tag them (@tagged("post_install", "-at_install")) to control when they run relative to installation.
What to assert: that constraints reject bad data (assertRaises), that computed fields calculate correctly, and that workflows change state as expected. Because each test is isolated by the rollback, you can freely create records in setUp without cleanup. Use self.env to access models exactly as production code does — tests exercise the real ORM, not mocks.
🏋️ Practical Exercise
- Write a
TransactionCasetest. - Test a model method.
- Assert a computed value.
- Run tests with
--test-enable. - Use test tags.
🔥 Challenge Exercise
Write a unit test (TransactionCase) that creates a record, calls a method, and asserts the result, then run it with --test-enable. Explain Odoo’s test classes (TransactionCase vs HttpCase) and why tests run in a rolled-back transaction.
- 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
Interview Questions
- How do you write tests in Odoo?
- What is
TransactionCase? - Why do Odoo tests roll back?
- How do you run a module’s tests?
- What is
HttpCaseused for?
Related Topics
FAQ
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.
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.
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.

