| name | odoo_backend_testing |
| description | Backend testing patterns for custom Odoo modules using xmlrpc scripts for post-installation validation. Use when creating backend tests for Odoo modules. |
| version | 1.0.0 |
| author | VPCS Team |
| category | testing |
| odoo_versions | ["17.0","18.0","19.0"] |
| tags | ["odoo","testing","xmlrpc","backend","validation","scripts"] |
Goal
Define post-installation/update backend test approach for custom Odoo modules using xmlrpc scripts for direct functionality validation, aligned with progress/task live-test loops.
Primary Approach: xmlrpc Scripts (Post-Install/Update)
Why xmlrpc?
- BEST WAY: RPC-based testing is the PREFERRED and BEST WAY to test custom Odoo apps. It validates real interaction logic after installation and bypasses the limitations of Odoo's built-in test runner.
- Direct: Tests real module behavior after installation/update (full RPC workflow).
- Executable: Runs standalone after
manage_modules.sh install/update without pytest overhead.
- Effective: Validates complete flows (CRUD, access rights, cron jobs, integrations) in single script.
- Maintainable: Stored in module's
/scripts/ folder alongside module code.
- Clear: Non-technical stakeholders can understand test intent and results.
Structure & Conventions
A. Script Location
<custom_module>/
__init__.py
__manifest__.py
## Goal
Define post-installation/update backend test approach for custom Odoo modules using xmlrpc/json2 scripts for direct functionality validation, aligned with progress/task live-test loops.
## Primary Approach: xmlrpc / JSON-2 Scripts (Post-Install/Update)
Why use scripts:
- Run after install/update to validate full workflows (CRUD, transitions, ACLs, cron jobs, integrations).
- Stored in the module `scripts/` folder and executable by the agent.
- Support both legacy XML-RPC (Odoo <=18) and JSON-2 API (Odoo 19+).
Script layout example:
```text
<custom_module>/
__init__.py
__manifest__.py
models/
views/
data/
security/
scripts/
__init__.py
test_workflows.py
test_integrations.py
test_migrations.py
Base xmlrpc test template (simplified):
"""
Module post-installation validation script.
Usage: python3 test_workflows.py [--url URL] [--db DB] [--user USER] [--password PASSWORD]
Credentials default to .env values (ODOO_URL, ODOO_DB_NAME, ODOO_DB_USER, ODOO_DB_PASSWORD)
"""
import xmlrpc.client
import sys
import os
from argparse import ArgumentParser
class OdooTestClient:
def __init__(self, url, db, user, password):
self.url = url
self.db = db
self.user = user
self.password = password
self.common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
self.object = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
self.uid = self.common.authenticate(db, user, password, {})
if not self.uid:
raise ValueError(f"Authentication failed for {user}@{db} on {url}")
def create(self, model, values):
return self.object.execute_kw(self.db, self.uid, self.password, model, 'create', [values])
():
..execute_kw(.db, .uid, .password, model, , [record_id], {: fields []})
():
args = args []
..execute_kw(.db, .uid, .password, model, method, [[record_id]] + args)
():
..execute_kw(.db, .uid, .password,
model, , [record_id], values)
():
..execute_kw(.db, .uid, .password,
model, , [domain])
():
args = args []
..execute_kw(.db, .uid, .password,
model, method, [[record_id]] + args)
Configuration (use .env)
- The agent MUST read
AgentSkills/.env (copy from env_template.txt) for runtime paths and credentials. Key variables:
- Paths:
ODOO_LOCAL_PATH / ODOO{X}_LOCAL_PATH, ODOO_MANAGE_SCRIPT, ODOO_CUSTOM_ADDONS, ODOO_LOG_FILE, FILESYSTEM_ROOT_PATH
- Database Credentials:
ODOO_DB_NAME, ODOO_DB_USER, ODOO_DB_PASSWORD, ODOO_API_KEY, ODOO_URL
- Version-specific:
ODOO17_DB_NAME, ODOO18_DB_NAME, ODOO19_DB_NAME, etc.
Example usage in xmlrpc scripts:
import os
url = os.getenv('ODOO_URL', 'http://localhost:8069')
db = os.getenv('ODOO_DB_NAME', 'odoo19')
user = os.getenv('ODOO_DB_USER', 'admin')
password = os.getenv('ODOO_DB_PASSWORD', 'admin')
api_key = os.getenv('ODOO_API_KEY', '')
auth_credential = api_key if api_key else password
If version-specific env vars exist (e.g., ODOO19_DB_NAME), prefer them for that version.
API Choice: XML-RPC vs JSON-2 (Odoo 19+)
- For Odoo 19+ prefer the JSON-2 API (
/json/2/...) when available because it's modern and supports API keys. Use XML-RPC for backward compatibility and when JSON-2 isn't available.
- Detection pseudocode:
if version >= 19 and endpoint_responds('/json/2'):
use_json2 = True
else:
use_json2 = False
When using JSON-2, tests should switch to HTTP requests calls and bearer API keys. Provide both templates in /scripts/ and prefer JSON-2 template when Odoo 19+.
Executing Tests (recommended agent workflow)
When .sandbox/session.json exists, use the controller contract:
SESSION_ID=$(python3 -c 'import json; print(json.load(open(".sandbox/session.json"))["session_id"])')
sandbox/bin/sandboxctl module "$SESSION_ID" install <module>
sandbox/bin/sandboxctl module "$SESSION_ID" test <module>
The newest result JSON must report succeeded. Do not run raw odoo-bin;
local mode continues to use bash manage_modules.sh.
- Ensure Odoo is running and port ready (see port readiness section below).
- Run the module's xmlrpc or json2 script from the module
scripts/ folder using the AgentSkills venv Python:
VENV_PY="$FILESYSTEM_ROOT_PATH/AgentSkills/.venv/bin/python"
cd "$ODOO_CUSTOM_ADDONS/<module>/scripts"
"$VENV_PY" python3 test_workflows.py --url http://127.0.0.1:8069 --db mydb --user admin --password admin
- Capture exit code and stdout/stderr. On failure, parse stacktrace and update progress file with error context.
Port readiness and verification
Before running tests, verify Odoo HTTP port (default 8069) is accepting connections:
curl -sSf http://127.0.0.1:8069/ -o /dev/null && echo "open" || echo "closed"
If closed, tail ODOO_LOG_FILE and surface the last 200 lines to help debugging.
Reporting test results to progress file
- After running tests, agent MUST append a
test_results entry to PROGRESS_DIR/<module>_progress.json containing:
"test_results": {
"timestamp": 1234567890,
"script": "scripts/test_workflows.py",
"stdout": "...",
"stderr": "...",
"exit_code": 1
}
Set sub_task status to complete or failed depending on exit_code and include retry_count if retries were attempted.
Retry policy and backoff
- Use up to 3 attempts with exponential backoff (2s, 5s, 10s). After 3 failed attempts mark the test sub-task as
failed and attach logs to progress file.
Example: Running JSON-2 test snippet (Odoo 19+)
import requests
url = 'http://127.0.0.1:8069/json/2/res.partner/search'
payload = {
'params': {
'domain': [],
'fields': ['name']
}
}
headers = {'Authorization': 'Bearer ' + API_KEY}
resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()
============================================================================
TEST FLOWS
============================================================================
def test_model_creation_and_validation(client):
"""Test 1: Create and validate custom model records."""
print("\\n[Test 1] Model Creation & Validation")
try:
record_id = client.create('custom_module.custom_model', {
'name': 'Test Record',
'description': 'Automated test via xmlrpc',
'status': 'draft',
})
print(f" ✓ Created record ID {record_id}")
record = client.read('custom_module.custom_model', record_id, ['name', 'status'])
assert record['status'] == 'draft', f"Status mismatch: {record['status']}"
print(f" ✓ Record validated: {record['name']} (status={record['status']})")
return True
except Exception as e:
print(f" ✗ FAILED: {e}")
return False
def test_workflow_transitions(client):
"""Test 2: Workflow state transitions and side effects."""
()
:
record_id = client.create(, {
: ,
: ,
})
()
result = client.call_method(, , record_id)
()
record = client.read(, record_id, [, , ])
record[] == ,
record[],
()
Exception e:
()
():
()
:
record_id = client.create(, {
: ,
: ,
})
()
record = client.read(, record_id, [, ])
()
Exception e:
()
():
()
:
crons = client.search(, [(, , )])
()
crons:
cron_id = crons[]
result = client.call_method(, , cron_id)
()
Exception e:
()
TEST FLOWS
Below are recommended test functions to include in scripts/test_workflows.py.
def test_model_creation_and_validation(client):
"""Test 1: Create and validate custom model records."""
print("\n[Test 1] Model Creation & Validation")
try:
record_id = client.create('custom_module.custom_model', {
'name': 'Test Record',
'description': 'Automated test via xmlrpc',
'status': 'draft',
})
print(f" ✓ Created record ID {record_id}")
record = client.read('custom_module.custom_model', record_id, ['name', 'status'])
assert record['status'] == 'draft', f"Status mismatch: {record['status']}"
print(f" ✓ Record validated: {record['name']} (status={record['status']})")
return True
except Exception as e:
print(f" ✗ FAILED: {e}")
return False
def test_workflow_transitions(client):
"""Test 2: Workflow state transitions and side effects."""
()
:
record_id = client.create(, {: , : })
()
client.call_method(, , record_id)
()
record = client.read(, record_id, [, , ])
record[] ==
record[]
()
Exception e:
()
():
()
:
record_id = client.create(, {: , : })
()
record = client.read(, record_id, [, ])
()
Exception e:
()
():
()
:
crons = client.search(, [(, , )])
()
crons:
cron_id = crons[]
client.call_method(, , cron_id)
()
Exception e:
()
():
()
:
record_id = client.create(, {: , : })
()
Exception e:
()
MAIN runner
def main():
default_url = os.getenv('ODOO_URL', 'http://localhost:8069')
default_db = os.getenv('ODOO_DB_NAME', 'odoo19')
default_user = os.getenv('ODOO_DB_USER', 'admin')
default_password = os.getenv('ODOO_DB_PASSWORD', 'admin')
parser = ArgumentParser(description='Custom module xmlrpc test suite.')
parser.add_argument('--url', default=default_url, help=f'Odoo server URL (default: {default_url})')
parser.add_argument('--db', default=default_db, help=f'Database name (default: {default_db})')
parser.add_argument('--user', default=default_user, help=f'Username (default: {default_user})')
parser.add_argument('--password', default=default_password, help=f'Password (default: from .env)')
args = parser.parse_args()
print(f"\n{'='*70}")
print("Testing Custom Module: post-install/update validation")
print(f"Server: {args.url} | DB: {args.db} | User: {args.user}")
print(f"{'='*70}")
try:
client = OdooTestClient(args.url, args.db, args.user, args.password)
Exception e:
()
sys.exit()
results = {
: test_model_creation_and_validation(client),
: test_workflow_transitions(client),
: test_acl_and_record_rules(client),
: test_cron_and_scheduled_actions(client),
: test_integrations_and_external_calls(client),
}
()
()
()
passed = ( v results.values() v)
total = (results)
test_name, result results.items():
status = result
()
()
passed == total
__name__ == :
sys.exit(main())
## Secondary Approach: Unit Tests (for regression/edge cases)
### When to use unit tests
- Edge cases and error paths (not covered by xmlrpc flow tests).
- Complex compute/constraint logic.
- Mocking external calls (APIs, payment gateways).
- Performance/load testing.
### Practices
- Use `--test-tags <module>` to isolate; keep tests deterministic.
- Arrange/Act/Assert pattern; cover happy path + error cases.
- Seed data with YAML/CSV/demo when needed; clean up after tests.
- Validate ACL/record rules explicitly (sudo vs non-sudo).
- **Odoo 19 Data**: `type='product'` is removed. Use `'type': 'consu'` and `'is_storable': True` for storable products.
### Unit Test File Location
<custom_module>/tests/
init.py
common.py # Test fixtures, test data
test_models.py # Model creation, compute, constraints
test_workflows.py # Workflow transitions, side effects
test_security.py # ACL, record rules, access denial
## Integration: LIVE TEST Sub-Task
### Step 1: Install/Update Module
```bash
cd /path/to/odoo_local_setup
./manage_modules.sh install custom_module --version 19
Step 2: Run xmlrpc Test Script
cd /path/to/custom_module/scripts
python3 test_workflows.py \
--url http://localhost:8019 \
--db odoo19 \
--user admin \
--password admin
Step 3: Verify Results
- All xmlrpc tests pass (5/5 ✓).
- No exceptions or connection errors.
- External integrations validated (webhooks, API calls logged).
Step 4: Mark as LIVE TEST Complete
{
"feature": "Custom Module Feature",
"sub_tasks": [
{"title": "Install module", "status": "done"},
{"title": "Run xmlrpc tests", "status": "done"},
{"title": "LIVE TEST", "status": "done"}
]
}
Key Differences: xmlrpc vs Unit Tests
| Aspect | xmlrpc Script | Unit Test |
|---|
| Scope | Full workflow (CRUD→validate→transition→side effects) | Single method/constraint |
| Trigger | Post-install/update (manual or CI/CD) | Before commit (--test-tags) |
| Execution | Standalone Python script (no pytest) | pytest framework |
| Mocking | Minimal; tests real module behavior | Heavy; isolates component |
| Speed | Slower (full RPC + DB ops) | Fast (in-memory, mocked) |
| Stakeholder | Can review test intent easily | Requires Python knowledge |
| Use Case | Acceptance testing, regression validation | Edge case coverage, refactoring safety |
Example: Custom App Dependency Testing
If custom_app depends on custom_lib:
- Create
/custom_lib/scripts/test_api.py to validate lib exports (functions, models).
- Create
/custom_app/scripts/test_workflows.py to test integration with custom_lib.
- Run both after install:
test_api.py → test_workflows.py.
- Mark LIVE TEST complete only if both pass.
Official Odoo Documentation References
Odoo 17.0 External API (XML-RPC)
- URL: https://www.odoo.com/documentation/17.0/developer/reference/external_api.html
- Authentication:
xmlrpc/2/common endpoint → authenticate(db, username, password, {}) returns uid
- Method calls:
xmlrpc/2/object endpoint → execute_kw(db, uid, password, model, method, args, kw={})
- Key methods: search, search_count, read, write, create, unlink, fields_get, search_read
- API Keys: Supported since Odoo 14.0; use API key instead of password
Odoo 18.0 External API (XML-RPC)
Odoo 19.0 External API (JSON-2 + XML-RPC Legacy)
Syntax Reference
Odoo 17/18 (XML-RPC)
import xmlrpc.client
url = "http://localhost:8017"
db = "odoo17"
username = "admin"
password = "admin"
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
object_rpc = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
uid = common.authenticate(db, username, password, {})
ids = object_rpc.execute_kw(db, uid, password, 'res.partner', 'search', [[['is_company', '=', True]]])
records = object_rpc.execute_kw(db, uid, password, 'res.partner', 'read', [ids], {'fields': ['name', 'email']})
id = object_rpc.execute_kw(db, uid, password, 'res.partner', 'create', [{'name': 'Test'}])
object_rpc.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {'name': 'Updated'}])
object_rpc.execute_kw(db, uid, password, 'custom_module.model', 'action_confirm', [[id]])
Odoo 19.0 (JSON-2 API - Recommended)
import requests
url = "https://mycompany.example.com/json/2"
api_key = "<your_api_key_here>"
db = "mycompany"
headers = {
"Authorization": f"bearer {api_key}",
"X-Odoo-Database": db,
"Content-Type": "application/json",
}
res = requests.post(
f"{url}/res.partner/search",
headers=headers,
json={"domain": [["is_company", "=", True]]}
)
ids = res.json()
res = requests.post(
f"{url}/res.partner/read",
headers=headers,
json={"ids": ids, "fields": ["name", "email"]}
)
records = res.json()
res = requests.post(
f"{url}/res.partner/create",
headers=headers,
json={"name": "Test"}
)
id = res.json()
res = requests.post(
f"{url}/custom_module.model/action_confirm",
headers=headers,
json={"ids": [id]}
)
result = res.json()
Summary
- Primary: xmlrpc scripts in
/scripts/ folder for post-install validation.
- Secondary: Unit tests for edge cases and regression safety.
- Integration: Both tied to progress.json LIVE TEST sub-task.
- Version Strategy:
- Odoo 17/18: Use XML-RPC (xmlrpc.client) via
/xmlrpc/2/common and /xmlrpc/2/object
- Odoo 19: Prefer JSON-2 API (requests library) via
/json/2/<model>/<method>; XML-RPC deprecated
- API Keys: Use instead of password for enhanced security (all versions)