# Odoo 16.0 Test Patterns and Best Practices This document provides comprehensive patterns and best practices for writing tests in Odoo 16.0 Enterprise modules, based on the Siafa project standards. ## Table of Contents 1. [Test Infrastructure](#test-infrastructure) 2. [Common Setup Patterns](#common-setup-patterns) 3. [Database Constraints](#database-constraints) 4. [Test Organization](#test-organization) 5. [Assertion Patterns](#assertion-patterns) 6. [Common Pitfalls](#common-pitfalls) ## Test Infrastructure ### Import Statement Always use the correct import for `TransactionCase`: ```python from odoo.tests.common import TransactionCase from odoo.exceptions import UserError, ValidationError ``` **Note:** Use `from odoo.tests.common import TransactionCase`, NOT `from odoo.tests import TransactionCase`. ### Base Test Class ```python class TestModuleName(TransactionCase): """Test cases for module functionality.""" def setUp(self): """Set up test data.""" super().setUp() # Use super().setUp() not super(ClassName, self).setUp() # Initialize models self.Model = self.env['model.name'] # Set up test data # ... ``` ## Common Setup Patterns ### Using Existing Records **Preferred Pattern:** Use existing database records when possible to avoid database constraint issues: ```python def setUp(self): super().setUp() # Use existing warehouse self.warehouse = self.env['stock.warehouse'].search([], limit=1) if not self.warehouse: self.skipTest("No warehouse available for testing") # Use existing product self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1) if not self.product: self.skipTest("No storable product available for testing") ``` ### Creating New Records with .sudo() When creating new records is necessary, use `.sudo()` to bypass access rights and permission issues: ```python def setUp(self): super().setUp() # Create vendor self.vendor = self.env['res.partner'].sudo().create({ 'name': 'Test Vendor', 'is_company': True, 'supplier_rank': 1, }) # Create product with minimal required fields self.product = self.env['product.product'].sudo().create({ 'name': 'Test Product', 'type': 'product', 'purchase_method': 'receive', 'list_price': 100.0, 'standard_price': 80.0, }) ``` ### Class-Level Setup For test data shared across all test methods, use `setUpClass`: ```python @classmethod def setUpClass(cls): """Set up test data shared across all test methods.""" super().setUpClass() # Create shared test data cls.vendor = cls.env['res.partner'].sudo().create({ 'name': 'Test Vendor', 'is_company': True, }) ``` ## Database Constraints ### Handling NOT NULL Constraints Many databases have custom NOT NULL constraints. Avoid creating records that trigger these constraints: **Problem:** Creating a product might fail due to missing `sale_line_warn` field: ```python # ❌ May fail with "null value in column 'sale_line_warn' violates not-null constraint" self.product = self.env['product.product'].create({ 'name': 'Test Product', 'type': 'product', }) ``` **Solution 1:** Use existing records: ```python # ✅ Use existing product from database self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1) ``` **Solution 2:** Provide all required fields (if known): ```python # ✅ Provide the required field self.product = self.env['product.product'].sudo().create({ 'name': 'Test Product', 'type': 'product', 'sale_line_warn': 'no-message', # Required in some databases }) ``` ### HTML/Markup Fields HTML fields return `Markup` objects, not plain strings: ```python # ❌ Fails: Markup('
Text
') != 'Text' self.assertEqual(record.comment, 'Updated comment') # ✅ Use a different field for testing self.assertEqual(record.barcode, 'TEST002') # ✅ Or convert to string self.assertIn('Updated comment', str(record.comment)) ``` ## Test Organization ### Test Method Naming Use descriptive names with numbering for execution order: ```python def test_01_create_record(self): """Test creating a new record.""" pass def test_02_update_record(self): """Test updating an existing record.""" pass def test_03_validation_error(self): """Test that validation error is raised for invalid data.""" pass ``` ### Test Coverage Areas Comprehensive tests should cover: 1. **CRUD Operations** - Create records with valid data - Update records - Read/search records - Delete records (if applicable) 2. **Validation Logic** - Test constraints - Test computed fields - Test onchange methods 3. **Business Logic** - Test state transitions - Test workflow methods - Test custom methods 4. **Edge Cases** - Test with empty data - Test with maximum values - Test with invalid combinations 5. **Error Handling** - Test that appropriate errors are raised - Test error messages are clear ## Assertion Patterns ### Basic Assertions ```python # Equality self.assertEqual(record.field, expected_value) self.assertNotEqual(record.field, unexpected_value) # Boolean self.assertTrue(record.active) self.assertFalse(record.archived) # Containment self.assertIn(item, collection) self.assertNotIn(item, collection) # String matching self.assertIn('substring', record.message) ``` ### Exception Testing ```python # Test that exception is raised with self.assertRaises(UserError) as context: record.forbidden_action() # Check exception message self.assertIn('Cannot perform action', str(context.exception)) ``` ### Recordset Assertions ```python # Check recordset size self.assertEqual(len(records), 3) # Check recordset is empty self.assertFalse(records) # Check record exists self.assertTrue(record) self.assertTrue(record.exists()) ``` ### Computed Field Testing ```python # Test computed field self.assertEqual(record.total_amount, 100.0) # Test compute dependency record.line_ids = [(5, 0, 0)] # Clear lines self.assertEqual(record.total_amount, 0.0) ``` ## Common Pitfalls ### 1. Product Creation Failures **Issue:** Creating products fails due to database constraints. **Solution:** Use existing products or create with `.sudo()` and minimal fields: ```python self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1) ``` ### 2. Incorrect Import Statement **Issue:** `from odoo.tests import TransactionCase` causes import errors. **Solution:** Use `from odoo.tests.common import TransactionCase` ### 3. Super() Call Format **Issue:** Using old-style super() calls. **Solution:** Use `super().setUp()` instead of `super(ClassName, self).setUp()` ### 4. Missing .sudo() **Issue:** Tests fail due to access rights. **Solution:** Use `.sudo()` when creating test data: ```python self.partner = self.env['res.partner'].sudo().create({...}) ``` ### 5. HTML Field Comparisons **Issue:** Comparing HTML fields directly fails. **Solution:** Use non-HTML fields or convert to string: ```python # Instead of comment field, use barcode or another text field self.assertEqual(record.barcode, 'TEST001') ``` ### 6. Transaction Isolation **Issue:** Tests affect each other due to shared data. **Solution:** Each test method runs in its own transaction (automatic with `TransactionCase`) ### 7. Insufficient Error Checking **Issue:** Not verifying error messages. **Solution:** Always check exception messages: ```python with self.assertRaises(UserError) as context: record.action() self.assertIn('expected error message', str(context.exception)) ``` ### 8. Missing Test Documentation **Issue:** Tests without docstrings are hard to understand. **Solution:** Always add descriptive docstrings: ```python def test_prevent_usage_change_with_moves(self): """Test that usage cannot be changed when location has stock moves.""" # ... ``` ## Running Tests ### Command Patterns ```bash # Run tests during module installation python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \ --test-enable --stop-after-init \ -i MODULE_NAME # Run tests during module update python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \ --test-enable --stop-after-init \ -u MODULE_NAME # Run with verbose output python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \ --test-enable --stop-after-init \ --log-level=test \ -u MODULE_NAME ``` ### Test Tags Tag tests for selective execution: ```python from odoo.tests import TransactionCase, tagged @tagged('post_install', '-at_install', 'module_name') class TestModuleName(TransactionCase): """Test cases with tags.""" pass ``` Run tagged tests: ```bash python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \ --test-enable --stop-after-init \ --test-tags module_name ```