Why Testing Matters (and Why It's Skipped)
Testing is the most important and most neglected part of Odoo development. Developers write models, views, and controllers under deadline pressure, promising to add tests later. Later never comes. Then a module upgrade breaks something, a security rule allows unauthorized access, or a computed field returns wrong values — and nobody catches it until a customer reports the issue. AI coding agents can generate comprehensive test suites from existing code, covering the cases developers meant to test but never did.
What AI Test Agents Generate
1. CRUD Tests
# AI reads your model and generates basic CRUD tests:
class TestRentalEquipment(TransactionCase):
"""Test rental.equipment model operations."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.category = cls.env['rental.equipment.category'].create({
'name': 'Power Tools',
})
cls.equipment = cls.env['rental.equipment'].create({
'name': 'Drill Press DP-500',
'serial_number': 'DP-500-001',
'category_id': cls.category.id,
'daily_rate': 45.00,
'weekly_rate': 250.00,
'monthly_rate': 800.00,
'state': 'available',
})
def test_create_equipment(self):
"""Test equipment creation with all required fields."""
self.assertEqual(self.equipment.name, 'Drill Press DP-500')
self.assertEqual(self.equipment.state, 'available')
self.assertEqual(self.equipment.daily_rate, 45.00)
def test_serial_number_unique(self):
"""Test that duplicate serial numbers are rejected."""
with self.assertRaises(Exception):
self.env['rental.equipment'].create({
'name': 'Another Drill',
'serial_number': 'DP-500-001', # duplicate
})
def test_required_fields(self):
"""Test that creation fails without required fields."""
with self.assertRaises(Exception):
self.env['rental.equipment'].create({
'serial_number': 'DP-500-002',
# missing 'name' — required
})2. Workflow Tests
# AI generates tests for state transitions:
class TestRentalAgreement(TransactionCase):
"""Test rental agreement workflow."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.customer = cls.env['res.partner'].create({
'name': 'Test Customer',
'email': '[email protected]',
})
cls.equipment = cls.env['rental.equipment'].create({
'name': 'Excavator EX-200',
'serial_number': 'EX-200-001',
'daily_rate': 350.00,
'state': 'available',
})
def test_agreement_draft_to_active(self):
"""Test confirming a rental agreement."""
agreement = self.env['rental.agreement'].create({
'partner_id': self.customer.id,
'equipment_id': self.equipment.id,
'start_date': fields.Date.today(),
})
self.assertEqual(agreement.state, 'draft')
agreement.action_confirm()
self.assertEqual(agreement.state, 'active')
self.assertEqual(self.equipment.state, 'rented')
def test_cannot_rent_unavailable_equipment(self):
"""Test that rented equipment can't be rented again."""
self.equipment.state = 'rented'
with self.assertRaises(ValidationError):
self.env['rental.agreement'].create({
'partner_id': self.customer.id,
'equipment_id': self.equipment.id,
'start_date': fields.Date.today(),
})
def test_return_releases_equipment(self):
"""Test that returning equipment sets it available."""
agreement = self._create_active_agreement()
agreement.action_return()
self.assertEqual(agreement.state, 'returned')
self.assertEqual(self.equipment.state, 'available')3. Access Rights Tests
# AI generates security tests — often the most neglected:
class TestRentalSecurity(TransactionCase):
"""Test access rights and record rules."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.rental_user = cls.env['res.users'].create({
'name': 'Rental User',
'login': 'rental_user',
'groups_id': [(6, 0, [
cls.env.ref('equipment_rental.group_rental_user').id
])],
})
cls.basic_user = cls.env['res.users'].create({
'name': 'Basic User',
'login': 'basic_user',
'groups_id': [(6, 0, [
cls.env.ref('base.group_user').id
])],
})
def test_rental_user_can_read(self):
"""Rental users can read equipment records."""
equipment = self.env['rental.equipment'].with_user(
self.rental_user
).search([])
self.assertTrue(len(equipment) >= 0) # no access error
def test_basic_user_cannot_access(self):
"""Users without rental group cannot access equipment."""
with self.assertRaises(AccessError):
self.env['rental.equipment'].with_user(
self.basic_user
).search([])
def test_rental_user_cannot_delete(self):
"""Regular rental users cannot delete equipment."""
equipment = self.env['rental.equipment'].create({
'name': 'Test', 'serial_number': 'TEST-001',
})
with self.assertRaises(AccessError):
equipment.with_user(self.rental_user).unlink()4. Computed Field Tests
# AI tests that computed fields return correct values:
def test_rental_charge_daily(self):
"""Test daily rate charge calculation."""
agreement = self._create_active_agreement()
agreement.start_date = fields.Date.today() - timedelta(days=3)
agreement.action_return()
# 3 days × $350/day = $1,050
self.assertEqual(agreement.total_charge, 1050.00)
def test_rental_charge_weekly_rate(self):
"""Test that weekly rate applies for 7+ day rentals."""
agreement = self._create_active_agreement()
agreement.start_date = fields.Date.today() - timedelta(days=10)
agreement.action_return()
# 1 week ($2,000) + 3 days ($1,050) = $3,050
self.assertEqual(agreement.total_charge, 3050.00)
def test_rental_charge_monthly_rate(self):
"""Test that monthly rate applies for 30+ day rentals."""
agreement = self._create_active_agreement()
agreement.start_date = fields.Date.today() - timedelta(days=35)
agreement.action_return()
# 1 month ($8,000) + 5 days ($1,750) = $9,750
self.assertEqual(agreement.total_charge, 9750.00)Edge Cases AI Catches
- Null/empty values: what happens when optional fields are empty?
- Boundary conditions: exactly 7 days (daily or weekly rate?)
- Concurrent access: two users renting the same equipment simultaneously
- Date edge cases: leap years, timezone boundaries, midnight crossings
- Large values: 999,999 quantity, very long text fields
- Special characters: product names with quotes, ampersands, unicode
Test Coverage Report
| Category | Without AI | With AI |
|---|---|---|
| CRUD operations | Sometimes tested | Always tested |
| Workflow transitions | Happy path only | Happy + error paths |
| Access rights | Rarely tested | All groups tested |
| Computed fields | Sometimes tested | All computations |
| Constraints | Rarely tested | All SQL + Python |
| Edge cases | Almost never | Common edges covered |
| Overall coverage | 20-40% | 70-85% |
AI Test Generation Workflow
- Point AI agent at your module directory
- AI reads models, views, and security files
- AI generates test file with comprehensive test cases
- Developer reviews, adjusts expected values, adds domain-specific tests
- Run tests:
odoo --test-enable -u your_module --stop-after-init - Fix any failures, iterate with AI for additional coverage
DeployMonkey AI Test Writer
DeployMonkey's AI coding agent generates test suites for your Odoo modules. Point it at your code and get comprehensive tests for models, workflows, security, computed fields, and edge cases. Run tests automatically on every deployment with DeployMonkey's CI pipeline.