Ad – 728×90
⚡ Advanced Topics

Testing Odoo Modules – Unit Tests, Common Cases, and Test Data

Odoo tests run inside a real database transaction that is rolled back after each test class. You write tests as Python classes that inherit TransactionCase, set up data in setUpClass(), and use standard Python assertEqual() / assertRaises() assertions. Tests run via odoo-bin -t.

⏱️ 22 min 🎯 Advanced 📅 Updated 2026
What you'll learn:
  • 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:

Python
# tests/__init__.py
from . import test_library_book
from . import test_library_loan
Python
# 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

ClassIsolationUse when
TransactionCaseEach test method runs in a savepoint (rolled back after)Most model tests — creates/writes real records, all rolled back
SavepointCase (deprecated)Like TransactionCase, older APIAvoid in new code
HttpCaseFull HTTP server — uses Werkzeug test clientTesting 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

Python
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)
Ad – 728×90

Testing Constraints

Python
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:

Python
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

Bash
# 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
Key takeaways:
  • Place tests in tests/test_*.py; import them in tests/__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.