Initial commit
This commit is contained in:
369
skills/odoo-test-creator/references/test_patterns.md
Normal file
369
skills/odoo-test-creator/references/test_patterns.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user