Skip to content

Testing

Testing strategies for Odoo modules and Field PWA.

Odoo Module Testing

Test File Structure

my_module/
└── tests/
    ├── __init__.py
    ├── test_my_model.py
    └── common.py          # Shared test utilities

tests/__init__.py:

from . import test_my_model

Writing Tests

tests/test_my_model.py:

# -*- coding: utf-8 -*-
from odoo.tests import TransactionCase, tagged
from odoo.exceptions import ValidationError


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

    @classmethod
    def setUpClass(cls):
        """Set up test data."""
        super().setUpClass()
        cls.partner = cls.env['res.partner'].create({
            'name': 'Test Partner',
            'email': 'test@example.com',
        })
        cls.my_model = cls.env['my.model'].create({
            'name': 'Test Record',
            'partner_id': cls.partner.id,
        })

    def test_create_record(self):
        """Test record creation."""
        record = self.env['my.model'].create({
            'name': 'New Record',
        })
        self.assertTrue(record.id)
        self.assertEqual(record.state, 'draft')

    def test_action_confirm(self):
        """Test confirm action changes state."""
        self.assertEqual(self.my_model.state, 'draft')
        self.my_model.action_confirm()
        self.assertEqual(self.my_model.state, 'confirmed')

    def test_action_done(self):
        """Test done action after confirm."""
        self.my_model.action_confirm()
        self.my_model.action_done()
        self.assertEqual(self.my_model.state, 'done')

    def test_validation_error(self):
        """Test validation raises error."""
        with self.assertRaises(ValidationError):
            self.env['my.model'].create({
                'name': '',  # Required field
            })

    def test_compute_field(self):
        """Test computed field calculation."""
        # Add test for computed fields
        self.my_model._compute_total()
        self.assertGreaterEqual(self.my_model.total_amount, 0)

Running Tests

# Run all tests for a module
docker compose exec odoo odoo \
  --test-enable \
  -i my_module \
  --stop-after-init \
  -d odoo_test

# Run specific test class
docker compose exec odoo odoo \
  --test-enable \
  --test-tags my_module \
  -i my_module \
  --stop-after-init \
  -d odoo_test

# Run with verbose output
docker compose exec odoo odoo \
  --test-enable \
  -i my_module \
  --stop-after-init \
  --log-level=test \
  -d odoo_test

Test Tags

@tagged('post_install', '-at_install')  # Run after install
@tagged('at_install')                    # Run during install
@tagged('-standard', 'slow')            # Custom tags

Common tags: | Tag | Description | |-----|-------------| | post_install | Run after module installation | | at_install | Run during module installation | | -at_install | Skip during installation | | standard | Standard test suite | | -standard | Exclude from standard suite |

Test Types

TransactionCase

Each test runs in a transaction that's rolled back. Fast, isolated.

from odoo.tests import TransactionCase

class TestFast(TransactionCase):
    def test_something(self):
        # Changes are rolled back after test
        pass

SavepointCase

Uses savepoints instead of full rollback. Good for tests that need commits.

from odoo.tests import SavepointCase

class TestSavepoint(SavepointCase):
    def test_something(self):
        # Uses savepoints for partial rollback
        pass

HttpCase

For testing HTTP endpoints and web controllers.

from odoo.tests import HttpCase

class TestHttp(HttpCase):
    def test_web_page(self):
        response = self.url_open('/my/endpoint')
        self.assertEqual(response.status_code, 200)

PWA Testing

Test Structure

pwa/
└── tests/
    ├── __init__.py
    ├── conftest.py       # Pytest fixtures
    ├── test_auth.py
    ├── test_jobs.py
    └── test_api.py

Pytest Configuration

conftest.py:

import pytest
from app import create_app


@pytest.fixture
def app():
    """Create test application."""
    app = create_app('testing')
    app.config['TESTING'] = True
    return app


@pytest.fixture
def client(app):
    """Create test client."""
    return app.test_client()


@pytest.fixture
def authenticated_client(client):
    """Create authenticated test client."""
    client.post('/login', data={
        'username': 'test_user',
        'password': 'test_pass'
    })
    return client

Writing PWA Tests

test_auth.py:

def test_login_page(client):
    """Test login page loads."""
    response = client.get('/login')
    assert response.status_code == 200
    assert b'Login' in response.data


def test_login_success(client, mocker):
    """Test successful login."""
    mocker.patch('services.odoo.authenticate', return_value={'uid': 1})
    response = client.post('/login', data={
        'username': 'admin',
        'password': 'admin'
    }, follow_redirects=True)
    assert response.status_code == 200


def test_login_failure(client, mocker):
    """Test failed login."""
    mocker.patch('services.odoo.authenticate', return_value=None)
    response = client.post('/login', data={
        'username': 'wrong',
        'password': 'wrong'
    })
    assert b'Invalid credentials' in response.data


def test_logout(authenticated_client):
    """Test logout."""
    response = authenticated_client.get('/logout', follow_redirects=True)
    assert response.status_code == 200

test_api.py:

import json


def test_api_health(client):
    """Test health endpoint."""
    response = client.get('/api/health')
    assert response.status_code == 200
    data = json.loads(response.data)
    assert data['status'] in ['healthy', 'degraded']


def test_api_jobs_unauthenticated(client):
    """Test jobs API requires auth."""
    response = client.get('/api/jobs')
    assert response.status_code == 401


def test_api_jobs_authenticated(authenticated_client, mocker):
    """Test jobs API returns data."""
    mocker.patch('services.odoo.get_jobs', return_value=[
        {'id': 1, 'name': 'Test Job'}
    ])
    response = authenticated_client.get('/api/jobs')
    assert response.status_code == 200
    data = json.loads(response.data)
    assert len(data['jobs']) == 1

Running PWA Tests

# Run all tests
docker compose exec pwa python -m pytest

# Run with coverage
docker compose exec pwa python -m pytest --cov=app --cov-report=html

# Run specific file
docker compose exec pwa python -m pytest tests/test_auth.py

# Run with verbose output
docker compose exec pwa python -m pytest -v

# Run specific test
docker compose exec pwa python -m pytest tests/test_auth.py::test_login_success

Manual Testing Checklist

Odoo Module

  • Module installs without errors
  • All menu items appear correctly
  • Forms load and save properly
  • Search and filters work
  • Actions/buttons function correctly
  • Security rules enforced (test as different users)
  • Reports generate correctly
  • No JavaScript console errors

PWA

  • Login/logout works
  • Dashboard loads
  • All pages accessible
  • Forms submit correctly
  • Offline mode works
  • Data syncs when online
  • Mobile responsive
  • No console errors

Integration

  • PWA connects to Odoo API
  • Data flows correctly between systems
  • Signatures upload to S3
  • SMS sends via JustCall

Test Coverage

Odoo Coverage

# Install coverage
pip install coverage

# Run with coverage
coverage run --source=extra-addons/odoo/my_module \
  odoo-bin --test-enable -i my_module --stop-after-init

# Generate report
coverage report
coverage html

PWA Coverage

docker compose exec pwa python -m pytest \
  --cov=app \
  --cov-report=term-missing \
  --cov-report=html

CI/CD Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  test-odoo:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: odoo
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3
      - name: Run Odoo Tests
        run: |
          docker compose -f docker-compose.yml -f docker-compose.test.yml up -d
          docker compose exec -T odoo odoo --test-enable -i my_module --stop-after-init

  test-pwa:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install -r pwa/requirements.txt pytest pytest-cov
      - name: Run tests
        run: cd pwa && pytest --cov=app

Debugging Failed Tests

Odoo

# Run with debug logging
docker compose exec odoo odoo \
  --test-enable \
  -i my_module \
  --stop-after-init \
  --log-level=debug \
  -d odoo_test 2>&1 | tee test.log

# Check log for errors
grep -i error test.log

PWA

# Run single test with output
docker compose exec pwa python -m pytest tests/test_auth.py -v -s

# Debug with pdb
docker compose exec pwa python -m pytest tests/test_auth.py --pdb