Files
2025-11-29 18:50:04 +08:00

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

  1. Test Infrastructure
  2. Common Setup Patterns
  3. Database Constraints
  4. Test Organization
  5. Assertion Patterns
  6. 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:

  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

# 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