Skip to content

Odoo 19 Testing: Complete Guide to TransactionCase, HttpCase & Tours

DeployMonkey Team · March 22, 2026 14 min read

Why Test Odoo Modules?

Untested Odoo modules break silently during upgrades, introduce regressions when modified, and cause data corruption in production. Testing catches these issues before they reach users. Odoo provides a comprehensive testing framework — use it.

Test Types

TypeBase ClassTestsSpeed
Unit (Model)TransactionCaseORM operations, business logic, constraintsFast
Unit (Saved)SavepointCaseSame as above, shared setup across testsFaster
HTTPHttpCaseControllers, API endpoints, web routesMedium
UI TourHttpCase + tourFull browser UI workflowsSlow

TransactionCase — Model Testing

# tests/test_equipment.py
from odoo.tests import TransactionCase, tagged
from odoo.exceptions import ValidationError


@tagged('post_install', '-at_install')
class TestEquipment(TransactionCase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.department = cls.env['hr.department'].create({
            'name': 'Engineering',
        })
        cls.employee = cls.env['hr.employee'].create({
            'name': 'Alice',
            'department_id': cls.department.id,
        })

    def test_create_equipment(self):
        """Test basic equipment creation."""
        equipment = self.env['equipment.item'].create({
            'name': 'Laptop #42',
            'serial_number': 'SN-042',
            'department_id': self.department.id,
            'assigned_to': self.employee.id,
        })
        self.assertEqual(equipment.name, 'Laptop #42')
        self.assertEqual(equipment.department_id, self.department)

    def test_serial_number_unique(self):
        """Serial number must be unique."""
        self.env['equipment.item'].create({
            'name': 'Item 1',
            'serial_number': 'SN-001',
        })
        with self.assertRaises(Exception):  # IntegrityError
            self.env['equipment.item'].create({
                'name': 'Item 2',
                'serial_number': 'SN-001',  # Duplicate
            })

    def test_computed_warranty_days(self):
        """Test warranty days computation."""
        from datetime import date, timedelta
        equipment = self.env['equipment.item'].create({
            'name': 'Test',
            'serial_number': 'SN-TEST',
            'warranty_expiry': date.today() + timedelta(days=30),
        })
        self.assertEqual(equipment.days_until_warranty, 30)

    def test_constraint_name_length(self):
        """Name must be at least 3 characters."""
        with self.assertRaises(ValidationError):
            self.env['equipment.item'].create({
                'name': 'AB',  # Too short
                'serial_number': 'SN-SHORT',
            })

HttpCase — Controller Testing

@tagged('post_install', '-at_install')
class TestEquipmentAPI(HttpCase):

    def test_api_list_equipment(self):
        """Test the equipment list API endpoint."""
        self.authenticate('admin', 'admin')
        response = self.url_open('/api/v1/equipment')
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertTrue(data.get('success'))

    def test_api_requires_auth(self):
        """Unauthenticated requests should fail."""
        response = self.url_open('/api/v1/equipment')
        self.assertIn(response.status_code, [401, 403])

Test Tags

# Run specific test tags:
# python odoo-bin -d testdb --test-tags /equipment_tracking

# Common tags:
@tagged('post_install', '-at_install')  # Run after module install
@tagged('post_install', 'at_install')   # Run both times
@tagged('-standard', 'slow')            # Excluded from standard runs

Running Tests

# Run all tests for a module
python odoo-bin -d testdb --test-tags /equipment_tracking --stop-after-init

# Run a specific test class
python odoo-bin -d testdb --test-tags /equipment_tracking.TestEquipment --stop-after-init

# Run with verbose output
python odoo-bin -d testdb --test-tags /equipment_tracking --log-level=test:DEBUG --stop-after-init

Test File Structure

your_module/
├── tests/
│   ├── __init__.py          # Import all test files
│   ├── test_equipment.py    # Model tests
│   ├── test_api.py          # Controller tests
│   └── test_security.py     # Access control tests
├── __init__.py
└── __manifest__.py
# tests/__init__.py
from . import test_equipment
from . import test_api
from . import test_security

Common Testing Patterns

Test Access Control

def test_user_cannot_delete(self):
    """Regular users cannot delete equipment."""
    user = self.env['res.users'].create({...})
    equipment = self.env['equipment.item'].sudo().create({...})
    with self.assertRaises(AccessError):
        equipment.with_user(user).unlink()

Test Computed Fields

def test_total_cost(self):
    """Total cost should sum line costs."""
    order = self.env['purchase.order'].create({...})
    # Create lines...
    self.assertAlmostEqual(order.total_cost, 1500.00, places=2)

Test Workflow Transitions

def test_confirm_workflow(self):
    """Confirming should change state and create delivery."""
    order = self.env['sale.order'].create({...})
    self.assertEqual(order.state, 'draft')
    order.action_confirm()
    self.assertEqual(order.state, 'sale')
    self.assertTrue(order.picking_ids)

AI-Assisted Test Writing

Claude Code generates comprehensive test suites from model definitions. It covers CRUD, constraints, computed fields, access control, and workflows — often generating more thorough tests than manual writing.

Getting Started

Deploy a test database on DeployMonkey and run your tests against it. Use Claude Code to generate initial test suites, then refine them manually for edge cases specific to your business logic.