Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:50:04 +08:00
commit 3e809e35ad
41 changed files with 10010 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
# 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('<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:
```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
```