8.7 KiB
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
- Test Infrastructure
- Common Setup Patterns
- Database Constraints
- Test Organization
- Assertion Patterns
- Common Pitfalls
Test Infrastructure
Import Statement
Always use the correct import for TransactionCase:
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
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:
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:
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:
@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:
# ❌ 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:
# ✅ Use existing product from database
self.product = self.env['product.product'].search([('type', '=', 'product')], limit=1)
Solution 2: Provide all required fields (if known):
# ✅ 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:
# ❌ Fails: Markup('<p>Text</p>') != '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:
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:
-
CRUD Operations
- Create records with valid data
- Update records
- Read/search records
- Delete records (if applicable)
-
Validation Logic
- Test constraints
- Test computed fields
- Test onchange methods
-
Business Logic
- Test state transitions
- Test workflow methods
- Test custom methods
-
Edge Cases
- Test with empty data
- Test with maximum values
- Test with invalid combinations
-
Error Handling
- Test that appropriate errors are raised
- Test error messages are clear
Assertion Patterns
Basic Assertions
# 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
# 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
# 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
# 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:
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:
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:
# 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:
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:
def test_prevent_usage_change_with_moves(self):
"""Test that usage cannot be changed when location has stock moves."""
# ...
Running Tests
Command Patterns
# 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:
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install', 'module_name')
class TestModuleName(TransactionCase):
"""Test cases with tags."""
pass
Run tagged tests:
python3 src/odoo-bin -c src/odoo.conf -d DATABASE_NAME \
--test-enable --stop-after-init \
--test-tags module_name